2022 Web Development Bootcamp

Section 32: Milestone Project: A Complete Online Shop - Applying Everything We Learned

olivia_yj 2022. 10. 6. 03:12

The goals

๐Ÿ’ช๐ŸปPlan The Project

โœŒ๐ŸปFrontend: HTML, CSS & Browser-side JavaScript

๐Ÿ‘๐ŸปBackend: NodeJS & MongoDB - Customer & Admin

 

 

So let's try to build our project step by step!

Installing the packages

Since we will use node, we initiate node program manager, and put '-y' which means we will say 'yes' for all the questions.

And of course we need express!

To make our server reopen itself every time when we update our code!

It's much convenient with 'nodemon'!

And we can change the script for starting server in package.json file

 

 

Make app.js file

Keep in mind: "app.js" is our main-entry file that starts the Node / Express server, handles (and distributes) incoming requests etc.

 

Listening on port 3000 means that we'll be able to send Http requests to <domain>:3000. 

On our local machine, that's "locahost:3000" (localhost is an alias for our local computer - it's only accessible from inside our own computer).

And we organize the structure a little bit!

Usually we use 'auth.js' file but since we might use this file in controllers folder, we make 'auth.routes.js' file in routes folder.

 

Check once again with MVC pattern

Our usual way has been like this. 

Make the route and put the route there and use funtion also, so every route we needed to define the variable to use it for the function and the code seems quite messy.

Now we have controller! which connects our models with views and routes by containing the functions!

 

We will export the functions which will be in this file as an object which we can use to group multipe values or functions together and here we can also add a key for each exporting function!

 

It looks much simple!

And now we need to go to app.js file to make it aware of these auth routes

 

 We use app.use which is a built-in method in the express app object that allows us to add a middleware that will be triggered for every incoming request before app starts listening because we do want to check for every incoming request if this request is for one of these two routes.

Therefore we want to use our middleware for every incoming request and we do it like this.

 

Our controller functions will then only be executed if the incoming request mathes the criteria laid out here, so if it's a get request for one of these two paths. 

 

Fix views with EJS engine

After installing ejs, we should notify our code that we will use EJS so go to app.js file and set 'view engine' as 'ejs' and the next line of it, we say where they should view (read), and the path to get to there.

However, to write the path for it, we should use 'path'. It is recommended that we use the built-in path package becuase this allows us to create a path that will be recognized on all operating systems. 

We want to start with a path to the overall project folder and nodejs has global variable for that!

It's __dirame variable

And as a second value of this path we write 'views' to construct an absolute path to this views folder in this project.

And we fix the file order a little bit, put 'auth', 'cart' and 'product' folders in customer folder which we newly made!

So we only have 'admin', 'customer' folders in views

 

 In views, we have includes folder to put the files will be applied commonly for the each url and this is 'head.ejs'.

Here I closed </head> tag but we shouldn't do this if we wanna include some code inside of <head> tag!

header.ejs looking

 

And now we move to signup.ejs file and we will include 'head.ejs' and 'header.ejs' files.

We use <%- %> NOT <%= %> cause we don't want to escape raw data!

or we can separate again only for the footer and just use ejs engine and include it also!

 

To be able to extract the email from the incoming request on the server, we also have to give this input 'name' attribute which is another built in HTML attribute on input elements, because the name which we assign here will be the key which we can later use on the server side to extract the value entered by the user in that input field.

It is quite common to use a paragraph instead of forms to basically mark and group our different form inputs and labels that belong together! 

 

Just fill it other information also!

 

And now connect it to the 'getSignup' function

 

Improve the looking of our webpage

Input the google font in our head.ejs file

And we made public folder and again made styles folder inside of it to put css files.

We don't have to set the href "../../../public/styles/base.css" cause we set it at app.js file so it will find this in public folder

So we should make an app.js file to know about the existence of public folder

 

Since we didn't close this <head> tag in head.ejs file we can import any link in each file.

When we import the style file, usually the order doesn't matter that much except the situation that if we use the same item in each file and try to overwrite it.

We set box-sizing to border-box this controls what the width of an element will be in the end, if the border and so on counts into the width or not. We got a behavior that makes working with widths a bit easier with border-box!

 

We can't use inheritance for setting the "box-sizing" property because that is a non-inheritable property! font-damily on the other hand IS inheritable!

 

 

 

Set CSS Variables for convenience!

It's much better using global CSS variable than using * (all selector) because if we use '*' it visits all the webpage to apply the styles but it we just use global CSS variable, it just applies the effect on webpage and this webpage will inherit the feature.

 

* {
  box-sizing: border-box;
}

html {
  font-family: "Montserrat", "sans-serif";

  --color-gray-50: rgb(243, 236, 230);
  --color-gray-100: rgb(207, 201, 195);
  --color-gray-300: rgb(99, 92, 86);
  --color-gray-400: rgb(70, 65, 60);
  --color-gray-500: rgb(37, 34, 31);
  --color-gray-600: rgb(32, 29, 26);
  --color-gray-700: rgb(31, 26, 22);

  --color-primary-50: rgb(253, 224, 200);
  --color-primary-100: rgb(253, 214, 183);
  --color-primary-200: rgb(250, 191, 143);
  --color-primary-400: rgb(223, 159, 41);
  --color-primary-500: rgb(212, 136, 14);
  --color-primary-700: rgb(212, 120, 14);
  --color-primary-200-contrast: rgb(100, 46, 2);
  --color-primary-500-contrast: white;

  --color-error-100: rgb(255, 192, 180);
  --color-error-500: rgb(199, 51, 15);

  --color-primary-500-bg: rgb(63, 60, 58);

  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-4: 1rem;
  --space-6: 1.5rem;
  --space-8: 2rem;

  --border-radius-small: 4px;
  --border-radius-medium: 6px;

  --shadow-medium: 0 2px 8px rgba(0, 0, 0, 0.2);
}
body {
  background-color: var(--color-gray-500);
  color: var(--color-gray-100);
  margin: 0;
}

And we can use our variable like this

 

For width and height, "%" refers to the available width / height of the parent element - in this case the <body> element (which is the parent of <main>)

 

h1 {
  text-align: center;
  color: var(--color-gray-300);
}

form {
  max-width: 25rem;
  margin: var(--space-8) auto;
  padding: var(--space-4);
  background-color: var(--color-gray-600);
  border-radius: var(--border-radius-medium);
  text-align: center;
}

form a {
  color: var(--color-primary-200);
}

form a:hover,
form a:active {
  color: var(--color-primary-400);
}

This is for 'auth.css' file

 

Below it's for forms.css

form hr {
  border-color: var(--color-primary-200);
  margin: var(--space-4);
}

label {
  color: var(--color-gray-100);
  display: block;
  margin-bottom: var(--space-2);
}

input, textarea {
  font: inherit;
  padding: var(--space-2);
  border-radius: var(--border-radius-small);
  border: none;
  width: 90%;
}

 

Try to work on database

function getSignup(req, res, next) {
   res.render('customer/auth/signup');
}

function signup(req, res) {

}

function getLogin(req, res, next) {
   
}

module.exports = {
  getSignup: getSignup,
  getLogin: getLogin,
  signup: signup
}
const express = require('express');

const authController = require('../controllers/auth.controller');

const router = express.Router();

router.get('/signup', authController.getSignup);

router.post('/signup', authController.signup);

router.get('/login', authController.getLogin);

module.exports = router;

If we will change the database of modify it, we should use 'post' method

const mongodb = require('mongodb');

const MongoClient = mongodb.MongoClient;

let database;

async function connectToDatabase() {
  const client = await MongoClient.connect('mongodb://localhost:27017');
  database = client.db('online-shop');
}

function getDb() {
  if (!database) {
    throw new Error('You must connect first!');
  }

  return database;
}

module.exports = {
  connectToDatabase: connectToDatabase,
  getDb: getDb
};

Now we will connect this to mongoDB
We should install mongodb package to use it conveniently!!

 

With "connect(...)", we connect to a MongoDB server - NOT to a single database. That happens in a second step with help of the "db(...)" method - this establishes a "connection" to a specific database on the overall database server.
const path = require("path");

const express = require("express");

const db = require("./data/database");
const authRoutes = require("./routes/auth.routes");

const app = express();

app.set("view engine", "ejs");
app.set("views", path.join(__dirname, "views"));

app.use(express.static("public"));

app.use(authRoutes);

db.connectToDatabase()
  .then(function () {
    app.listen(3000);
  })
  .catch(function (error) {
    console.log("Failed to connect to the database!");
    console.log(error);
  });

And use this database in our app.js code to be aware of our database existence!

We used 'then-catch' here 

 

Creating models

const bcrypt = require('bcryptjs');

const db = require('../data/database');

class User {
  constructor(email, password, fullname, street, postal, city) {
    this.email = email;
    this.password = password;
    this.name = fullname;
    this.address = {
      street: street,
      postalCode: postal,
      city: city
    };
  }

  async signup() {
    const hashedPassword = await bcrypt.hash(this.password, 12);

    await db.getDb().collection('users').insertOne({
      email: this.email,
      password: hashedPassword,
      name: this.name,
      address: this.address
    });
  }
}

module.exports = User;

In class user construction, we define many variables there but only email and password would be mandatory, because if we just give only email and password to this class then other value will be automatically set as 'undefined' so it is optional technically!

 

Objects can be created with "object literals" (-> {}) or with help of classes. Both approaches work - with classes, you create objects based on a clearly defined "blueprint" (with clearly defined properties and maybe also methods), with object literals, you create objects "on the fly" without any pre-determined structure.

 

"Methods" (i.e. functions that are bound to an object) can be added to objects that were created without a class as a blueprint too. BUT: if you define a method on a class, EVERY objects that's based on that class will automatically have that method - a pretty powerful and useful feature!

 

async signup() {
    const hashedPassword = await bcrypt.hash(this.password, 12);

    await db.getDb().collection('users').insertOne({
      email: this.email,
      password: hashedPassword,
      name: this.name,
      address: this.address
    });
  }

"address" will be a nested object / document. This is absolutely fine and common when working with NoSQL / MongoDB databases. In a SQL database, you might instead work with a separate "addresses" table where you then connect users to addresses via "address_id" fields.

 

We can encrypt the password with hashing but after encrypting we can't take it back with decrypting!

 

We usually import third-party package first and then our own package!

 

app.use(express.static("public"));
app.use(express.urlencoded({extended: false}));

It's quite regular to set extended as false to only support a regular form submission.

 

const User = require('../models/user.model');

function getSignup(req, res) {
  res.render('customer/auth/signup');
}

async function signup(req, res) {
  const user = new User(
    req.body.email,
    req.body.password,
    req.body.fullname,
    req.body.street,
    req.body.postal,
    req.body.city
  );

  await user.signup();

  res.redirect('/login');
}

function getLogin(req, res) {
  res.render('customer/auth/login');
}

module.exports = {
  getSignup: getSignup,
  getLogin: getLogin,
  signup: signup,
};

Now we can fix our auth.controller.js to have working functions

 

Since we fixed signup, now let's make login function!

For the security, let's installl csurf first

 

const path = require('path');

const express = require('express');
const csrf = require('csurf');

const db = require('./data/database');
const addCsrfTokenMiddleware = require('./middlewares/csrf-token');
const authRoutes = require('./routes/auth.routes');

const app = express();

Import middleware

app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));

app.use(express.static('public'));
app.use(express.urlencoded({ extended: false }));

app.use(csrf());

app.use(addCsrfTokenMiddleware);

app.use(authRoutes);

db.connectToDatabase()
  .then(function () {
    app.listen(3000);
  })
  .catch(function (error) {
    console.log('Failed to connect to the database!');
    console.log(error);
  });

Why we have two middlewares that deal with CSRF tokens: The third-party middleware ("csurf") helps us generate the token + checks incoming tokens for validity - our only middleware just distributes generated tokens to all our other middleware / route handler functions and views.

 

Since we will keep using this security tools, we just made a folder for middlewares and put csrf-token.js file!

And we can check in above code that we imported our own middleware!

 

Now we go to the signup.ejs to use csurf!

We used input type "hidden" which is one of the built-in types. 

This is an input which is not visible to the user and which is not meant for the user to be populated with data, but which instead is already there and sent along with the request!

This has to have a special name for which the CSRF package will look and that's "_csrf"

 

And we will also put this for login.ejs

 

But still it doesn't work well cause we don't have any session management!

 

So now we will go to the app.js to fix error handling code!

First of all, we should write a code to generate our own middleware to handle errors and we can just separate it to organize

 

So we made error-handler code in middlewares 

And make 500.ejs file to show when the error occurs and we put it in 'shared' folder and move 'includes' folder also inside of it.

And since we moved 'includes' folder, we should fix some routes we set for the includes!

 

<%- include('../../shared/includes/head', { pageTitle: 'Signup' }) %>
  <link rel="stylesheet" href="/styles/forms.css">
  <link rel="stylesheet" href="/styles/auth.css">
</head>
<body>
  <%- include('../../shared/includes/header') %>

Like this!

 

And now go to the app.js file

const db = require('./data/database');
const addCsrfTokenMiddleware = require('./middlewares/csrf-token');
const errorHandlerMiddleware = require('./middlewares/error-handler');
const authRoutes = require('./routes/auth.routes');
app.use(csrf());

app.use(addCsrfTokenMiddleware);

app.use(authRoutes);

app.use(errorHandlerMiddleware);

To use these middlewares!

 

And since 500.ejs is right next to includes folder since we moved it to be together in Shared folder our 500.ejs code will refer includes code directly, don't have to go inside the routes.

 

<%- include('includes/head', { pageTitle: 'An error occurred' }) %>
</head>
<body>
  <%- include('includes/header') %>
  <main>
    <h1>Something went wrong!</h1>
    <p>Unfortunately, something went wrong - please try again later.</p>
    <p><a class="btn" href="/">Back to safety!</a></p>
  </main>
<%- include('includes/footer') %>

 

Session Management

We need to install two packages to get help with session management, express-session and connect-mongodb-session

first thing is to manage session with express and second one is to save the session into the mongodb we are using it now

 

And to manage this, we make config folder and put session.js file inside.

Here we set the function for saving the session into our database and also the other function to set an configurate our session

We need to write an object here cause the express session package which we will use wants an object with all the configuration settings that we can set for creating such a session

 

const expressSession = require('express-session');
const mongoDbStore = require('connect-mongodb-session');

function createSessionStore() {
  const MongoDBStore = mongoDbStore(expressSession);

  const store = new MongoDBStore({
    uri: 'mongodb://localhost:27017',
    databaseName: 'online-shop',
    collection: 'sessions'
  });

  return store;
}

function createSessionConfig() {
  return {
    secret: 'super-secret',
    resave: false,
    saveUninitialized: false,
    store: createSessionStore(),
    cookie: {
      maxAge: 2 * 24 * 60 * 60 * 1000
    }
  };
}

module.exports = createSessionConfig;

And we set for the cookie validation would be expired in 2 days.

 

Now since we fixed session and validation we will apply these to sign up and login

We write a code for a user model and apply this in auth.controller

 

Adding user input validation on the backend

function isEmpty(value) {
  return !value || value.trim() === '';
}

function userCredentialsAreValid(email, password) {
  return (
    email && email.includes('@') && password && password.trim().length >= 6
  );
}

function userDetailsAreValid(email, password, name, street, postal, city) {
  return (
    userCredentialsAreValid(email, password) &&
    !isEmpty(name) &&
    !isEmpty(street) &&
    !isEmpty(postal) &&
    !isEmpty(city)
  );
}

function emailIsConfirmed(email, confirmEmail) {
  return email === confirmEmail;
}

module.exports = {
  userDetailsAreValid: userDetailsAreValid,
  emailIsConfirmed: emailIsConfirmed,
};

 

<%- include('../../shared/includes/head', { pageTitle: 'Login' }) %>
  <link rel="stylesheet" href="/styles/forms.css">
  <link rel="stylesheet" href="/styles/auth.css">
</head>
<body>
  <%- include('../../shared/includes/header') %>
  <main>
    <h1>Login</h1>
    <% if (inputData.errorMessage) { %>
      <section class="alert">
        <h2>Invalid Credentials</h2>
        <p><%= inputData.errorMessage %></p>
      </section>
    <% } %>
    <form action="/login" method="POST">
      <input type="hidden" name="_csrf" value="<%= locals.csrfToken %>">
      <p>
        <label for="email">E-Mail</label>
        <input type="email" id="email" name="email" value="<%= inputData.email %>" required>
      </p>
      <p>
        <label for="password">Password</label>
        <input type="password" id="password" name="password" value="<%= inputData.password %>" required>
      </p>
      <button class="btn">Login</button>
      <p id="switch-form"><a href="/signup">Create a new user</a></p>
    </form>
  </main>
<%- include('../../shared/includes/footer') %>

How to give administrator position to user

We can control this on database!

 

res.locals

The res.locals is an object that contains the local variables for the response which are scoped to the request only and therefore just available for the views rendered during that request or response cycle.

This property is useful while exposing the request-level information such as the request path name, user settings, authenticated user, etc.

 

Next vs Then in node.js

  • next() : It will run or execute the code after all the middleware function is finished.
  • return next() : By using return next it will jump out the callback immediately and the code below return next() will be unreachable.

In the function app.use((req, res, next), we have three callbacks i.e. request, response and next.

So, if you want to use next() then simply write next() and if you want to use return next then simply write return next().

Let’s understand these two by an example.

Using next(): If you have any middleware function and below the next() you have some lines or function that you want to execute, then by using next() you can actually execute the lines or function because it runs the code below next() after all middleware function finished.

Using return next(): If you have any middleware function and below the return next() you have some lines that you want to execute, then the lines which are below return next() won’t be executed because it will jump out the callback immediately and the code below return next() in the callback will be unreachable. 

 

import express from "express"

const app = express()
// API for the testing of next()
app.get(
'/next', function (req,res,next) {
	console.log('hi there ');
	next();
	console.log('you are still here');
}
)

// API for the testing of return next()
app.get(
'/return-next', function (req,res,next) {
	console.log('hi there');
	return next();
	console.log('you are still here');
}
)

app.listen(5000,()=> {
console.log("App is running on port 5000")
})

Output :

  1. next(): Hit ‘http://localhost:5000/next’ in your browser.
    Here the line which is below the next() is executed successfully and “you are still here” is shown in the output.
  2. return next(): Hit ‘http://localhost:5000/return-next’ in your browser.
    Here the line which is below the return next() is not executed and “you are still here” is not shown in the output.
์ž๋ฐ” ์Šคํฌ๋ฆฝํŠธ & ๋ถ€๋™ ์†Œ์ˆ˜์  [91์ผ์ฐจ]

์ด์ „ ๊ฐ•์˜์— -3.55271...(10์‹œ 10๋ถ„)์˜ ์žฅ๋ฐ”๊ตฌ๋‹ˆ์— ๋Œ€ํ•œ newTotalPrice๋ฅผ ๊ฐ„๋žตํ•˜๊ฒŒ ๋ณผ ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

๋ฌด์—‡์— ๊ด€ํ•œ ๊ฑธ๊นŒ์š”?

์‹ค์ œ ์ˆซ์ž๋Š” -3.55271...*e-10์œผ๋กœ ์•„์ฃผ ์ž‘์€ ์ˆซ์ž์˜€์Šต๋‹ˆ๋‹ค(์ฆ‰, <some number> x e^-10์œผ๋กœ ํ‘œํ˜„๋˜๋Š” ๊ต‰์žฅํžˆ ์ž‘์€ ์ˆซ์ž).

๊ทธ๋Ÿฐ๋ฐ ์ด ์ž‘์€ ์ˆ˜๋Š” ์–ด๋””์—์„œ ์™”์„๊นŒ์š”? 59.99 - 59.99๋Š” ์•„์ฃผ ์ž‘์€ ์ˆซ์ž๊ฐ€ ์•„๋‹ˆ๋ผ 0์„ ์ƒ์„ฑํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋งž์ฃ ?

๋ฌธ์ œ๋Š” ์ปดํ“จํ„ฐ๊ฐ€ ๋ถ„์ˆ˜(์ฆ‰, ์ •์ˆ˜๊ฐ€ ์•„๋‹Œ ์ˆซ์ž => ์†Œ์ˆ˜์  ์ดํ•˜ ์ž๋ฆฟ์ˆ˜๊ฐ€ ์žˆ๋Š” ์ˆซ์ž)๋กœ ์ˆ˜ํ•™์„ ํ•  ๋•Œ ๊ทธ๋‹ค์ง€ ์ข‹์ง€ ์•Š๋‹ค๋Š” ๊ฒ๋‹ˆ๋‹ค.

์ด "์˜ค๋ฅ˜" ๋’ค์— ์ˆจ๊ฒจ์ง„ ์ด๋ก ์— ๋Œ€ํ•ด ์ž์„ธํžˆ ์•Œ์•„๋ณด๋ ค๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋ธ”๋กœ๊ทธ ๊ฒŒ์‹œ๋ฌผ์„ ํƒ์ƒ‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. https://modernweb.com/what-every-javascript-developer-should-know-about-floating-points/

์ผ์ƒ ์—…๋ฌด์—์„œ ๋” ์ค‘์š”ํ•œ ์ ์€ ๊ทธ๋Ÿฌํ•œ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์ดํ•ดํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์ด๊ฑด ์•„์ฃผ ๊ฐ„๋‹จํ•˜์ฃ . ๊ฐ’์„ ์ถœ๋ ฅํ•˜๊ณ  ์ด ์ž‘์€ ์ •๋ฐ€๋„ ์˜ค๋ฅ˜(์ถœ๋ ฅ์—์„œ)๋ฅผ ํ”ผํ•˜๋ ค๋ฉด ์ˆซ์ž์—์„œ toFixed(<์†Œ์ˆ˜์  ์ž๋ฆฟ์ˆ˜>)๋ฅผ ํ˜ธ์ถœํ•ด์„œ ์ด๋ฅผ ๋‹ฌ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด 5.121321.toFixed(2)๋Š” 5.12๋ฅผ ์‚ฐ์ถœํ•ฉ๋‹ˆ๋‹ค => ๋‹ค๋ฅธ ์†Œ์ˆ˜ ์ž๋ฆฟ์ˆ˜๋Š” ๋‹จ์ˆœํžˆ ์ž˜๋ฆฝ๋‹ˆ๋‹ค.

 

 

sources

https://kmyjn.tistory.com/708

 

[JS] fetch API๋ฅผ ํ™œ์šฉํ•˜์—ฌ promise ์ดํ•ดํ•˜๊ธฐ (then, catch)

" data-ke-type="html"> <>HTML ์‚ฝ์ž… ๋ฏธ๋ฆฌ๋ณด๊ธฐํ•  ์ˆ˜ ์—†๋Š” ์†Œ์Šค JSON Placeholder API๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์˜ˆ์ œ์ฝ”๋“œ ์‚ดํŽด๋ณด๊ธฐ ์–ธ์ œ ๋๋‚ ์ง€ ๋ชจ๋ฅด๋Š” ์„œ๋ฒ„์™€์˜ ํ†ต์‹ ๊ณผ ๊ด€๋ จ๋œ ์ฒ˜๋ฆฌ๋“ค์€ ๋น„๋™๊ธฐ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•œ๋‹ค. ์ด๋•Œ promise

kmyjn.tistory.com

https://academind.com/tutorials/reference-vs-primitive-values

 

Reference vs Primitive Values

Learn why most people copy objects and arrays in JavaScript incorrectly. And why you won't make that mistake!

academind.com