Modern JavaScript Applications
上QQ阅读APP看书,第一时间看更新

Creating the monolithic core

We have finished creating the services and image storage server. The users interact with the monolithic core to view coupons and the admin interacts with the monolithic core to view unverified coupons, and then it either rejects or accepts a coupon. Other than new coupon submission by the user, everything else by the user and admin is done in the monolithic core.

Open the Initial/monolithic directory. Inside the directory, you will find a package.json file and an app.js file. The app.js file is where you will write the code, and package.json lists the dependencies for the monolithic core. The monolithic core is dependent on express, seneca, request and basic-auth npm packages. Run the npm install command inside Initial/monolithic to install the dependencies locally.

We will use the ejs template engine with Express. Inside the views directory, you will find ejs files for home, new coupon submit forms, and admin pages. The files already contain the templates and HTML code. The site is designed using Bootstrap.

The following is the code to import the modules:

var seneca = require("seneca")();
var express = require("express");
var app = express();
var basicAuth = require("basic-auth");
var request = require("request");

Now, for the monolithic core to be able to communicate with the database and url- config services, we need to add them to the monolithic core seneca instance. The following is the code to do this:

seneca.client({port: "5020", pin: {role: "url-config"}});
seneca.client({port: "5010", pin: {role: "coupons-store"}});

Now we need to set ejs as the view engine. Here is the code to set ejs as the view engine:

app.set("view engine", "ejs");

All the static files such as CSS, JS, and fonts are kept on the public directory. We need to serve them to the client. Here is the code to serve the static files:

app.use(express.static(__dirname + "/public"));

Here we are serving the static files in the same way as we served the static files (that is, images) in the image upload server.

Now we need to add a route to the server of the home page of our website that displays the first 20 coupons. It also displays the Next and Previous buttons to navigate between the next or previous 20 buttons.

The home page is accessed via the root URL. The following is the code to add a route to the server of the home page:

app.get("/", function(httpRequest, httpResponse, next){
  if(httpRequest.query.status == "submitted") {
    seneca.act({role: "coupons-store", cmd: "list", skip: 0}, function(err, coupons){
      if(err) return httpResponse.status(500).send("An error occured");

      seneca.act({role: "url-config", cmd: "image-storage-service"}, function(err, image_url){
        if(err) return httpResponse.status(500).send("An error occured");

        if(coupons.length > 20)
        {
          var next = true;
        }
        else
        {
          var next = false;
        }

        var prev = false;

        httpResponse.render("index", {prev: prev, next: next, current: 0, coupons: coupons, image_url: image_url.value, submitted: true});
      })
    })
    
    return;
  };

  if(parseInt(httpRequest.query.current) !== undefined && httpRequest.query.next == "true")
  {
    seneca.act({role: "coupons-store", cmd: "list", skip: parseInt(httpRequest.query.current) + 20}, function(err, coupons){
      if(err) return httpResponse.status(500).send("An error occured");

      seneca.act({role: "url-config", cmd: "image-storage-service"}, function(err, image_url){
        if(err) return httpResponse.status(500).send("An error occured");

        if(coupons.length > 20)
        {
          var next = true;
        }
        else
        {
          var next = false;
        }

        var prev = true;

        httpResponse.render("index", {prev: prev, next: next, current: parseInt(httpRequest.query.current) + 20, coupons: coupons, image_url: image_url.value});
      })
    })
  }
  else if(parseInt(httpRequest.query.current) != undefined && httpRequest.query.prev == "true")
  {
    seneca.act({role: "coupons-store", cmd: "list", skip: parseInt(httpRequest.query.current) - 20}, function(err, coupons){
      if(err) return httpResponse.status(500).send("An error occured");

      seneca.act({role: "url-config", cmd: "image-storage-service"}, function(err, image_url){
        if(err) return httpResponse.status(500).send("An error occured");

        if(coupons.length > 20)
        {
          var next = true;
        }
        else
        {
          var next = false;
        }

        if(parseInt(httpRequest.query.current) <= 20)
        {
          var prev = false;
        }
        else
        {
          prev = true;
        }

        httpResponse.render("index", {prev: prev, next: next, current: parseInt(httpRequest.query.current) - 20, coupons: coupons, image_url: image_url.value});
      })
    })
  }
  else
  {
    seneca.act({role: "coupons-store", cmd: "list", skip: 0}, function(err, coupons){
      if(err) return httpResponse.status(500).send("An error occured");

      seneca.act({role: "url-config", cmd: "image-storage-service"}, function(err, image_url){
        if(err) return httpResponse.status(500).send("An error occured");

        if(coupons.length > 20)
        {
          var next = true;
        }
        else
        {
          var next = false;
        }

        var prev = false;

        httpResponse.render("index", {prev: prev, next: next, current: 0, coupons: coupons, image_url: image_url.value});
      })
    })
  }
});

The index.ejs file is the view of the home page of our site. The preceding code renders this view to generate the final HTML code for the home page.

The preceding code implements pagination by checking whether prev or next keys are present in the query string. If these keys are undefined, then it displays the first 20 coupons, otherwise it calculates the skip value argument by adding 20 to the value of the current key in the query string.

Then, the code checks whether the total number of coupons retrieved is 21 or less. If they are less than 21, then it doesn't display the Next button by assigning the next variable to false, otherwise it displays the next button by assigning the next variable to true. However, the total number of coupons it displays is 20. We retrieved an extra coupon to just check whether we should display the next button or not. To find out whether we should display the previous button or not is fairly easy, that is, if the next key is true in the query string, then we must display the previous button.

The preceding code also checks for the status=submitted query string that indicates the user was redirected back from the upload service. If it's present, then it assigns the submitted local variable for the view to true. This is the ejs template present in the view that checks whether the submitted local variable is true or undefined and displays a successful form submission message:

<% if(typeof submitted !== "undefined"){ %>
  <% if(submitted == true){ %>
    <div class="alert alert-success" role="alert">Coupon has been submitted. Our administrator will review and the coupon shortly.</div>
  <% } %>
<% } %> 

Here is the ejs template present in the view that displays the coupons and the next and previous buttons:

<% if(coupons.length < 21){ %>
  <% var cut = 0; %>
<% } %>
<% if(coupons.length == 21){ %>
  <% var cut = 1; %>
<% } %>
<% for(var i = 0; i < coupons.length - cut; i++) {%>
  <div class="col-sm-3 col-lg-3 col-md-3">
    <div class="thumbnail">
      <img src="<%= image_url + '/' + coupons[i].thumbnail_id %>" alt="">
      <div class="caption">
        <h4 class="pull-right"><del><%= coupons[i].price %></del> <%= coupons[i].discount %></h4>
        <h4><a href="<%= coupons[i].url %>"><%= coupons[i].title %></a>
        </h4>
        <p><%= coupons[i].desc %></p>
      </div>
    </div>
  </div>
<% } %>
</div>

<ul class="pager">
<% if(prev == true){ %>
  <li class="previous"><a href="/?prev=true&current=<%= current %>">Previous</a></li>
<% } %>
<% if(next == true){ %>
  <li class="next"><a href="/?next=true&current=<%= current %>">Next</a></li>
<% } %>
</ul>

We are done creating our home page. Now we need to create a route with the /add URL path that will display a form to submit a new coupon. The view for this coupon submission page is add.ejs. Here is the code to create the route:

app.get("/add", function(httpRequest, httpResponse, next){
  seneca.act({role: "url-config", cmd: "upload-service"}, function(err, response){
    if(err) return httpResponse.status(500).send("An error occured");

    httpResponse.render("add", {upload_service_url: response.value});
  })
});

Here we are retrieving the base URL of the upload service from the URL config service and assigning it to the upload_service_url local variable so that the form knows where to submit the POST request.

The following is the template in the add.ejs view that displays the coupon submission form:

<form role="form" method="post" action="<%= upload_service_url %>/submit" enctype="multipart/form-data">
  <div class="form-group">
    <label for="email">Your Email address:</label>
    <input type="email" class="form-control" id="email" name="email">
  </div>
  <div class="form-group">
    <label for="title">Product Title:</label>
    <input type="text" class="form-control" id="title" name="title">
  </div>
  <div class="form-group">
    <label for="desc">Product Description:</label>
    <textarea class="form-control" id="desc" name="desc"></textarea>
  </div>
  <div class="form-group">
    <label for="url">Product URL: </label>
    <input type="text" class="form-control" id="url" name="url">
  </div>
  <div class="form-group">
    <label for="price">Original Price:</label>
    <input type="text" class="form-control" id="price" name="price">
  </div>
  <div class="form-group">
    <label for="discount">Discount Price:</label>
    <input type="text" class="form-control" id="discount" name="discount">
  </div>
  <div class="form-group">
    <label for="thumbnail">Product Image: <i>(320 x 150)</i></label>
    <input type="file" class="form-control" id="thumbnail" name="thumbnail">
  </div>
  <button type="submit" class="btn btn-default">Submit</button>
</form>

Now we need to provide a path for the site admin to access the admin panel. The path to access admin panel is going to be /admin. The admin panel will be protected using HTTP basic authentication.

We will create two more routes that will be used by the admin to accept or reject a coupon. The routes are /admin/accept and /admin/reject.

The following is the code to protect the admin panel using the HTTP basic authentication:

var auth = function (req, res, next){
  var user = basicAuth(req);

  if (!user || !user.name || !user.pass) 
  {
    res.set("WWW-Authenticate", "Basic realm=Authorization Required");
    res.sendStatus(401);
  }

  //check username and password
  if (user.name === "narayan" && user.pass === "mypassword") 
  {
    next();
  } 
  else 
  {
    res.set("WWW-Authenticate", "Basic realm=Authorization Required");
    res.sendStatus(401);
  }
}

app.all("/admin/*", auth);
app.all("/admin", auth);

Here we are executing the auth callback for all the admin panel paths. The callback checks whether the user is logged in or not. If user is not logged in, we will ask the user to log in. If the user tries to log in, then we will check whether the username and password is correct. If the username and password are wrong, we will ask the user to log in again. We will parse the HTTP basic authentication based the headers using the basic-auth module, that is, we will pass the req object to the basicAuth function to parse it. Here we are hardcoding the username and password.

Now we need to define the routes to access the admin panel. The admin.ejs file is the view for the admin panel. The following is the code to add the routes:

app.get("/admin", function(httpRequest, httpResponse, next){
  seneca.act({role: "coupons-store", cmd: "admin_list", skip: 0}, function(err, coupons){
    if(err) return httpResponse.status(500).send("An error occured");

    seneca.act({role: "url-config", cmd: "image-storage-service"}, function(err, image_url){
      httpResponse.render("admin", {coupons: coupons, image_url: image_url.value});
    });
  });
});

app.get("/admin/accept", function(httpRequest, httpResponse, next){
  seneca.act({role: "coupons-store", cmd: "verified", id: httpRequest.query.id}, function(err, verified){
    if(err) return httpResponse.status(500).send("An error occured");

    if(verified.value == true)
    {
      httpResponse.redirect("/admin");  
    }
    else
    {
      httpResponse.status(500).send("An error occured");
    }
  });
});

app.get("/admin/reject", function(httpRequest, httpResponse, next){
  seneca.act({role: "url-config", cmd: "image-storage-service"}, function(err, storage_server_url){
    if(err) return httpResponse.status(500).send("An error occured");

    request.get(storage_server_url.value + "/delete/" + httpRequest.query.thumbnail_id, function(err, resp, body){
      if(err) return httpResponse.status(500).send("An error occured");

      seneca.act({role: "coupons-store", cmd: "delete", id: httpRequest.query.id}, function(err, deleted){
        if(err) return httpResponse.status(500).send("An error occured");

        if(deleted.value == true)
        {
          httpResponse.redirect("/admin");  
        }
        else
        {
          httpResponse.status(500).send("An error occured");
        }
      });
    });
  })
});

When the admin visits /admin, unverified coupons are displayed along with buttons to accept or reject a coupon. When the admin clicks on the Accept button, then a request is made to the /admin/accept path to mark the coupon as verified, and when the admin clicks on the Reject button, a request is made to the /admin/reject path to delete the coupon. After accepting or deleting a coupon, the admin is redirected to the /admin path.

The following is the template that displays the coupons and verification buttons to the admin:

<% for(var i = 0; i < coupons.length; i++) {%>
  <tr>
    <td><%= coupons[i].title %></td>
    <td><%= coupons[i].desc %></td>
    <td><%= coupons[i].url %></td>
    <td><img style="width: 300px !important" src="<%= image_url + '/' + coupons[i].thumbnail_id %>" alt=""></td>
    <td><%= coupons[i].price %></td>
    <td><%= coupons[i].discount %></td>
    <td>
      <form role="form" method="get" action="/admin/accept">
        <div class="form-group">
          <input type="hidden" value="<%= coupons[i].id %>" name="id">
          <input type="hidden" value="<%= coupons[i].thumbnail_id %>" name="thumbnail_id">
          <input type="submit" value="Accept" class="btn btn-default">
        </div>
      </form>
    </td>
    <td>
      <form role="form" method="get" action="/admin/reject">
        <div class="form-group">
          <input type="hidden" value="<%= coupons[i].id %>" name="id">
          <input type="hidden" value="<%= coupons[i].thumbnail_id %>" name="thumbnail_id">
          <input type="submit" value="Reject" class="btn btn-default">
        </div>
      </form>
    </td>
  </tr>
<% } %>

We have defined our routes. Finally, we need to start the Express server. Here is the code to do so:

app.listen(9090);

Now go ahead and run the monolithic core server using the node app.js command.