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¤t=<%= current %>">Previous</a></li> <% } %> <% if(next == true){ %> <li class="next"><a href="/?next=true¤t=<%= 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.