Fullstack Hub

[Count: 1]

This is the last part of a series where we will develop the Search functionality for our website. Later, we will implement the Authentication Guard for admin section, a user won’t be able to go to the admin section without login. We will also add the HttpIntercepter to automatically embed the token in HTTP requests. In the end, we will update Express.js APIs to take care of JWT and deployment preparation steps.

Introduction

In this part, we will implement the search functionality for our website, we already have developed the Search API when creating the Page APIs on the server side. The search textbox is on the right side of the menu bar with a Search button next to it. This search would be performed on all dynamic pages generated by Page Management in the admin section. We will also implement the Authentication Guard for admin section, though we implemented the login/logout functionality in the previous part still user is able to directly browse the admin pages. This Auth Guard will make sure user is login before redirecting to the admin page. We will also secure the server side APIs with JWT to avoid unauthenticated user access. 

Let’s Start

It is strongly recommended to read all previous parts before moving to this one, I can’t promise you would understand all the steps I am going to list down without knowledge of previous parts.

  1. Make sure the previous part is working fine without any error.
  2. While updating the AppComponent for Login/Logout functionality, we also added the code for Search, so let’s go back to AppComponent, edit the src -> app.component.html and look for searchFrm, we have only one control, a text box for search and a Search button next to it. The Search button is bound with onSubmit() function that we will understand in the typescript file. Edit the src -> app.component.ts and find the searchFrm that we are initializing in ngOnInit lifecycle hook. We have only one control TextSearch with at least three characters required validation to search. The onSubmit function is pretty straightforward, it is taking the search string entered by the user and sending it to SearchPageComponent as a query string. Let’s create the SearchPageComponent and define its route searchPage.
  3. Right click on the mazharncoweb folder and select the option, Open in Terminal, in the TERMINAL tab, run the command: ng g c client/searchPage
  4. Edit the src -> app -> client -> search-page -> search-page.component.css and add the following CSS:  
mat-card
{
  margin: 0px;
  padding: 0px;
}

mat-card-noshadow{ 
    background: #ECECF4;
    box-shadow: none !important;
}

mat-card-content{
  margin: 0px;
  padding: 0px;
}

.header
{
background: #45484d;
border-bottom: 5px solid #393B3E;
height: 50px;
padding-left: 5px;
}

.subheader
{
height: 40px;
background: #DEE7F4;
}

.subheader_title{
    vertical-align:baseline;
    padding-top: 5px;
    padding-bottom: 0px;
    padding-left: 5px;
    font-size: 13pt;
    font-family: Arial, Helvetica, sans-serif;
    color: #C8CCD2;
}

.header_title{
    vertical-align:baseline;
    padding-top: 10px;
    padding-bottom: 0px;
    padding-left: 5px;
    font-size: 16pt;
    font-family: Arial, Helvetica, sans-serif;
    color: #A1A9AF;
}

.card_cntnt
{
    padding: 15px;

}
.content_header
{
    font-size:16pt;
    color: coral;
  
}
.content_subheader
{
    color: #908482;
}
  1. Edit the src -> app -> client -> search-page -> search-page.component.html and replace its content with the following: 
<mat-card>
  <mat-card-header class="header">
    <mat-card-title class="header_title">
      <i class="material-icons">find_in_page</i>Following pages contain your searched text: "{{searchString}}"
    </mat-card-title>
  </mat-card-header>
  <mat-card-content class="card_cntnt">
    <div style="padding:10px">
      <div class="row" *ngIf="(pages && pages.length>0);else norecord">
        <div *ngFor="let content of pages" style="margin-bottom:20px">
          <mat-card>
            <mat-card-header class="subheader">
              <mat-card-title class="header_title">{{content.Title}}</mat-card-title>
            </mat-card-header>
            <mat-card-content class="card_cntnt">
              <div [innerHTML]="content.Content.substring(0,500) | keepHtml"></div>...........
              <div><button style="background-color:#41454c" mat-raised-button color="accent" (click)="viewPage(content.MenuCode)"><i class="material-icons">pageview</i> Go to Page</button></div>
            </mat-card-content>
          </mat-card>
        </div>
      </div>

      <ng-template #norecord>
        <div class="row page_cntnt">
          <mat-card style="margin-bottom:20px">
            <mat-icon>info</mat-icon><span style="vertical-align:top"> There is no result for your search!</span>
          </mat-card>
        </div>
      </ng-template>

    </div>
  </mat-card-content>
</mat-card>
  1. This page will list down all the pages that have user-entered search string with the Go to Page button at the bottom that will redirect to the corresponding page, there is plenty of room to improve this page e.g. highlight the search string in the page, etc. but I just want to keep it simple for now and maybe will do it in future. 
  2. You can see in <div *ngFor="let content of pages" style="margin-bottom:20px"> line, we have pages object that would return from the server, we are traversing through each record,
    1. Displaying the page title,
    2. Displaying the first 500 characters of page content following by … 
    3. At the bottom, there is a button to go to the actual page, the viewPage() function takes the menu code and redirects it to the corresponding page.
    4. If there is no result found, the There is no result for your search! message would be displayed, we are using Angular if-else condition for it. 
  3. Edit the src -> app -> client -> search-page -> search-page.component.ts and replace its content with the following:
TypeScript
  1. Let’s try to understand it:
    1. On the top, we are specifying the search page API URL and other helping objects.
    2. In the constructor, we are injecting DataServiceActivatedRoute and Router services where _dataService is used to call the search API, a route is used to get the query string parameter i.e. is the search string coming from search textbox on the menu bar in AppComponent and router is used to redirect the user to the actual page once user will click on Go to Page button.
    3. The searchPage() function is taking the search string, calling the search API using DataService get method and storing the result in pages object that we are using in search-page.component.html to traverse each record. 
    4. The viewPage() function is taking the menu code and redirecting to the corresponding page, in view-page.component.ts you can see that we are taking the menu code as an incoming parameter and loading the page by menu code (the id is actually a menu code). 
  2. The next thing is to add the route for search page in app-routing.module.ts, go ahead and edit the src -> app -> app-routing.module.ts and replace its content with the following: 
TypeScript
  1. In the end, we added the searchPage route with search parameter that would be replaced with user entered a search string, you search page functionality is ready, enter at least three characters in the search textbox and hit the Search button, you should see the result and able to go to the page from there. Here, it is important to see the server -> models -> pageMdl.js line; PageSchema.index({ '$**': 'text' }); this line actually indexes the page document fields, definitely, it is expensive to do add and update operations but much faster and helpful to search within the content. Just consider it as a full-text search functionality in MS SQL Server if you are from a .NET background like me. 
  2. Next, let’s create the authentication Guard to stop the unauthenticated user to browse the admin section. In mazharncoweb TERMINAL tab, run the command: ng g guard guard/auth -m app.module.ts
  3. Edit the src -> app -> guard -> auth.guard.ts and replace its content with the following:  
TypeScript
  1. We are creating the AuthGuard class implementing a CanActivate interface that has only one method canActivate(),
    1. In the constructor, we are injecting the AuthService that we created in previous parts. 
    2. in canActivate() function, we are calling the isLoggedIn() method from AuthService that will tell us either user is logged in are not by checking the token expiration date in LocalStorage. (Check that function in AuthService)
  2. Now that we have Auth Guard, next step is to apply that Guard to admin pages route, edit the src -> app -> app-routing.module.ts once again and replace its content with the following:  
TypeScript
  1. Check the route starting with admin, we are applying the newly created AuthGuard by canActivate: [AuthGuard], this is how simple it is to apply Guard, now try to access the admin page without login, it won’t allow you to get in, later we will implement the error page with the proper message.
  2. We applied the logic to prevent user accessing the admin section without login but still server-side APIs implemented in Express.js are directly accessible without any authentication, e.g. use Postman to insert menu or page record in database by accessing its API (e.g. http://localhost:3000/api/menu POST request), you would successfully able to create it, how to fix this issue to put a check that user should be login to access the admin pages related API? The answer is token-based authentication, we will send the token with admin APIs that would be verified against the private key on the server side and if they do match, perform the corresponding operation, otherwise reject it as an unauthenticated user. In order to do it, we have to make two changes, one on the client side and other on the server side:
    1. Client Side: We need to embed the token with all authentication required APIs e.g. admin APIs. We will do it by HttpIntercepter class provided in Angular 5 onward (I guess).
    2. Server Side: We need to provide the JWT authentication middleware in APIs, this would be done through the express-jwt package that we will install if it is not there.
  3. On the client side, right click on src -> app -> service -> auth folder and selection option, New File, enter the name auth.interceptor.ts and add following code in it:   
TypeScript
  1. We are creating the AuthIntercepter class implementing the HttpIntercepter that has one method intercept(), in intercept() method we are actually cloning the Http Request and adding the token in the header that we are getting from AuthService where it is being fetched from LocalStorage. Why cloning? Because you cannot intercept the Http request due to security reason (that’s what I know, you can read the more about it online, but it does make sense to me because anybody can forge your request otherwise). 
  2. We also need to add this intercepter in AppModule, edit the src -> app -> app.module.ts and replace its content with the following: 
TypeScript
  1. Check the providers‘ section at the end, we are specifying the HTTP_INTERCEPTORS with our newly created AuthIntercepter class, that’s it. You can use Fiddler or any other traffic monitoring tool to monitor the request from a client to server to seethe if a token is embedded in APIs calls.
  2. Since we are working on the client side, it’s quick to create the error page to show the message to an unauthenticated user if s/he tries to access the admin section. Run the command in client TERMINAL tab: ng g c error
  3. Edit the src -> app -> error -> error.component.css and add following CSS in it:
mat-card
{
  margin: 0px;
  padding: 0px;
}

mat-card-noshadow{ 
    background: #ECECF4;
    box-shadow: none !important;
}

mat-card-content{
  margin: 0px;
  padding: 0px;
}

.header
{
background: #df0f0f;
border-bottom: 5px solid #3d0404;
height: 50px;
padding-left: 5px;
}

.subheader
{
height: 40px;
background: #5B5C60;
}

.subheader_title{
    vertical-align:baseline;
    padding-top: 5px;
    padding-bottom: 0px;
    padding-left: 5px;
    font-size: 13pt;
    font-family: Arial, Helvetica, sans-serif;
    color:red;
}

.header_title{
    vertical-align:baseline;
    padding-top: 10px;
    padding-bottom: 0px;
    padding-left: 5px;
    font-size: 16pt;
    font-family: Arial, Helvetica, sans-serif;
    color: #FFF;
}

.card_cntnt
{
    padding: 15px;
    padding-bottom: 15px;
    font-weight: bold;
}

.card_cntnt_scoll
{
    padding-top:5px;
    padding-bottom: 5px;
    height: 150px;
    overflow-y: auto;
}

.card_size{
    height:100px;
}
.sub_card_size
{
    height: 60px;
}
.icon
{
    vertical-align:bottom; 
    text-shadow: 1px 1px 2px #928B8B, 0 0 25px #46464A, 0 0 5px #BFBFCA;
}

  .leftRs {
      position: absolute;
      margin: auto;
      top: 0;
      bottom: 0;
      width: 50px;
      height: 50px;
      box-shadow: 1px 2px 10px -1px rgba(22, 3, 3, 0.30);
      border-radius: 999px;
      left: 0;
  }

  .rightRs {
      position: absolute;
      margin: auto;
      top: 0;
      bottom: 0;
      width: 50px;
      height: 50px;
      box-shadow: 1px 2px 10px -1px rgba(38, 5, 5, 0.30);
      border-radius: 999px;
      right: 0;
  }

  .carsoul_img{
    max-width: 150px;;
    height: 130px;
    text-align: center;
  }

  .clients_slider
  {
      background-color: rgb(51, 53, 57);
      color: #D1D1DB;
      text-decoration:none;
  }

  *, *:before, *:after {
    box-sizing: border-box;
  }
  
  html, body {
    height: 100%;
  }
  
  body {
    color: #444;
    font-family: 'Roboto', sans-serif;
    font-size: 1rem;
    line-height: 1.5;
  }
  
  .slider {
    height: 100%;
    position: relative;
    overflow: hidden;
    display: -webkit-box;
    display: -webkit-flex;
    display: -ms-flexbox;
    display: flex;
    -webkit-flex-flow: row nowrap;
        -ms-flex-flow: row nowrap;
            flex-flow: row nowrap;
    -webkit-box-align: end;
    -webkit-align-items: flex-end;
        -ms-flex-align: end;
            align-items: flex-end;
    -webkit-box-pack: center;
    -webkit-justify-content: center;
        -ms-flex-pack: center;
            justify-content: center;
  }
  .slider__nav {
    width: 12px;
    height: 12px;
    margin: 2rem 12px;
    border-radius: 50%;
    z-index: 10;
    outline: 6px solid #ccc;
    outline-offset: -6px;
    box-shadow: 0 0 0 0 #333, 0 0 0 0 rgba(51, 51, 51, 0);
    cursor: pointer;
    -webkit-appearance: none;
       -moz-appearance: none;
            appearance: none;
    -webkit-backface-visibility: hidden;
            backface-visibility: hidden;
  }
  .slider__nav:checked {
    -webkit-animation: check 0.4s linear forwards;
            animation: check 0.4s linear forwards;
  }
  .slider__nav:checked:nth-of-type(1) ~ .slider__inner {
    left: 0%;
  }
  .slider__nav:checked:nth-of-type(2) ~ .slider__inner {
    left: -100%;
  }
  .slider__nav:checked:nth-of-type(3) ~ .slider__inner {
    left: -200%;
  }
  .slider__nav:checked:nth-of-type(4) ~ .slider__inner {
    left: -300%;
  }
  .slider__inner {
    position: absolute;
    top: 0;
    left: 0;
    width: 400%;
    height: 100%;
    -webkit-transition: left 0.4s;
    transition: left 0.4s;
    display: -webkit-box;
    display: -webkit-flex;
    display: -ms-flexbox;
    display: flex;
    -webkit-flex-flow: row nowrap;
        -ms-flex-flow: row nowrap;
            flex-flow: row nowrap;
  }
  .slider__contents {
    height: 100%;
    padding: 2rem;
    text-align: center;
    display: -webkit-box;
    display: -webkit-flex;
    display: -ms-flexbox;
    display: flex;
    -webkit-box-flex: 1;
    -webkit-flex: 1;
        -ms-flex: 1;
            flex: 1;
    -webkit-flex-flow: column nowrap;
        -ms-flex-flow: column nowrap;
            flex-flow: column nowrap;
    -webkit-box-align: center;
    -webkit-align-items: center;
        -ms-flex-align: center;
            align-items: center;
    -webkit-box-pack: center;
    -webkit-justify-content: center;
        -ms-flex-pack: center;
            justify-content: center;
  }
  .slider__image {
    font-size: 2.7rem;
        color: #2196F3;
  }
  .slider__caption {
    font-weight: 500;
    margin: 2rem 0 1rem;
    text-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
    text-transform: uppercase;
  }
  .slider__txt {
    color: #999;
    margin-bottom: 3rem;
    max-width: 300px;
  }
  
  @-webkit-keyframes check {
    50% {
      outline-color: #333;
      box-shadow: 0 0 0 12px #333, 0 0 0 36px rgba(51, 51, 51, 0.2);
    }
    100% {
      outline-color: #333;
      box-shadow: 0 0 0 0 #333, 0 0 0 0 rgba(51, 51, 51, 0);
    }
  }
  
  @keyframes check {
    50% {
      outline-color: #333;
      box-shadow: 0 0 0 12px #333, 0 0 0 36px rgba(51, 51, 51, 0.2);
    }
    100% {
      outline-color: #333;
      box-shadow: 0 0 0 0 #333, 0 0 0 0 rgba(51, 51, 51, 0);
    }
  }
  
  1. Edit the src -> app -> error -> error.component.html and add following HTML in it:  
<mat-card class="card_size mat-card-noshadow">
  <mat-card-header class="header">
    <mat-card-title class="header_title"><i class="material-icons" style="vertical-align: middle">error</i> Error</mat-card-title>
  </mat-card-header>
  <mat-card-content class="card_cntnt">
    <div>
      <p>
        Sorry, you are not allowed to access this page.
      </p>
    </div>
  </mat-card-content>
</mat-card>
  1. Very simple with one message for an unauthenticated user, you can make it generic error page for all kind of errors if you want, feel free to play with it.
  2. Edit the src -> app -> error -> error.component.ts and add following code in it:  
TypeScript
  1. Nothing in it, play with it if you want to make this page to show all kinds of error, maybe a bunch of if conditions by checking error types, etc.
  2. Now that we have ErrorComponent, let’s update the routing table, edit the src -> app -> app-routing.module.ts and replace its content with the following:
TypeScript
  1. In the end, we added the ErrorComponent path error. In the case of unauthenticated use, we want to redirect the user to ErrorComponent as we discussed earlier, for that, we will update our AuthGurad, edit the src -> app -> guard -> auth.guard.ts and replace its content in with the following:  
TypeScript
  1. In canActivate() function, we added the two line at the end, that is simply redirecting the user to ErrorComponent if this._authService.isLoggedIn() function returns false.
  2. The last step is on the server side to add the jwt authentication on admin APIs, right click on server folder and select an option, Open in Terminal, in TERMINAL tab, run the command: npm install express-jwt --save
  3. We need to update the two files page.ts and menu.js in routes folder. Edit the server -> routes -> menu.js and replace its content with the following: 
JS
  1. Check the PUT, POST and DELETE APIs, there is jwt({ secret: config.secret }) parameters that have a functionality to check the public (token) and private key comparison and allow or disallow the user to move further. 
  2. Edit the server -> routes -> page.ts and replace its content with the following:  
JS
  1. Same goes to page.ts, check the POST, PUT and DELETE APIs and you would see jwt authentication code in the parameters. 
  2. That’s it folks for this series, run both client and server side and check your application. You should have a fully functional and secure application. If you have any errors, please don’t hesitate to contact me. 
  3. The final question is how to deploy it so there is an awesome video available on YouTube for it from Traversy Media that I followed to deploy my MEAN stack applications on Digital Ocean. 
  4. These are my prep steps for deployment:
    1. Build the client application by the command: ng build --prod, this will create the dist folder in mazharncoweb.
    2. Create the views folder in server and paste all files from mazharncoweb -> dist folder
    3. If you see server -> mazharnco.js file, in res.render(path.join(__dirname, './views/index.html')); we are telling our node.js application where to find the pages to view.
    4. Now your node.js application is ready, you don’t need to run the Angular client application, just run the command: pm2 start environment.json --env development and browse the http://localhost:3000 URL in the browser and you should be good to go because we built the client application and pasted the minified, uglified and AOT (ahead of time) compiled pages in server -> views, directed the node.js to get HTML pages from there.
    5. Since now, your application is a pure node.js application, no Angular crap, you are ready to follow Traversy Media video to deploy it on Digital Ocean. You can follow any tutorial to deploy it on your desired server e.g. Windows, Linux or Unix, there are plenty of online videos available. 
  5. Please follow the link to understand how to install MongoDB on Ubuntu. 
  6. That’s all for this series, I hope you guys will find it beneficial, for any concern, question or suggestion, please don’t hesitate to contact me at admin@fullstackhub.io. Happy Coding!

Yaseer Mumtaz

Leave a Reply

Your email address will not be published. Required fields are marked *