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.
- Make sure the previous part is working fine without any error.
- While updating the
AppComponent
for Login/Logout functionality, we also added the code for Search, so let’s go back toAppComponent
, edit the src -> app.component.html and look forsearchFrm
, we have only one control, a text box for search and a Search button next to it. The Search button is bound withonSubmit()
function that we will understand in the typescript file. Edit the src -> app.component.ts and find thesearchFrm
that we are initializing inngOnInit
lifecycle hook. We have only one controlTextSearch
with at least three characters required validation to search. TheonSubmit
function is pretty straightforward, it is taking the search string entered by the user and sending it toSearchPageComponent
as a query string. Let’s create theSearchPageComponent
and define its routesearchPage
. - 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
- 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;
}
- 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>
- 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.
- You can see in
<div *ngFor="let content of pages" style="margin-bottom:20px">
line, we havepages
object that would return from the server, we are traversing through each record,- Displaying the page title,
- Displaying the first 500 characters of page content following by …
- 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. - 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.
- Edit the src -> app -> client -> search-page -> search-page.component.ts and replace its content with the following:
- Let’s try to understand it:
- On the top, we are specifying the search page API URL and other helping objects.
- In the
constructor
, we are injectingDataService
,ActivatedRoute
andRouter
services where_dataService
is used to call the search API, aroute
is used to get the query string parameter i.e. is the search string coming from search textbox on the menu bar inAppComponent
androuter
is used to redirect the user to the actual page once user will click on Go to Page button. - The
searchPage()
function is taking the search string, calling the search API usingDataService
get method and storing the result inpages
object that we are using in search-page.component.html to traverse each record. - 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 (theid
is actually a menu code).
- 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:
- 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. - 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
- Edit the src -> app -> guard -> auth.guard.ts and replace its content with the following:
- We are creating the
AuthGuard
class implementing aCanActivate
interface that has only one methodcanActivate()
,- In the
constructor
, we are injecting theAuthService
that we created in previous parts. - in
canActivate()
function, we are calling theisLoggedIn()
method fromAuthService
that will tell us either user is logged in are not by checking the token expiration date in LocalStorage. (Check that function inAuthService
)
- In the
- 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:
- Check the route starting with admin, we are applying the newly created
AuthGuard
bycanActivate: [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. - 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:
- 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). - 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.
- Client Side: We need to embed the token with all authentication required APIs e.g. admin APIs. We will do it by
- 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:
- We are creating the
AuthIntercepter
class implementing theHttpIntercepter
that has one methodintercept()
, inintercept()
method we are actually cloning the Http Request and adding the token in the header that we are getting fromAuthService
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). - We also need to add this intercepter in
AppModule
, edit the src -> app -> app.module.ts and replace its content with the following:
- Check the
providers
‘ section at the end, we are specifying theHTTP_INTERCEPTORS
with our newly createdAuthIntercepter
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. - 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
- 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);
}
}
- 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>
- 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.
- Edit the src -> app -> error -> error.component.ts and add following code in it:
- 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.
- 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:
- In the end, we added the
ErrorComponent
path error. In the case of unauthenticated use, we want to redirect the user toErrorComponent
as we discussed earlier, for that, we will update ourAuthGurad
, edit the src -> app -> guard -> auth.guard.ts and replace its content in with the following:
- In
canActivate()
function, we added the two line at the end, that is simply redirecting the user toErrorComponent
ifthis._authService.isLoggedIn()
function returns false. - 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
- 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:
- 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. - Edit the server -> routes -> page.ts and replace its content with the following:
- Same goes to page.ts, check the POST, PUT and DELETE APIs and you would see jwt authentication code in the parameters.
- 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.
- 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.
- These are my prep steps for deployment:
- Build the client application by the command:
ng build --prod
, this will create the dist folder in mazharncoweb. - Create the views folder in server and paste all files from mazharncoweb -> dist folder
- 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. - 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. - 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.
- Build the client application by the command:
- Please follow the link to understand how to install MongoDB on Ubuntu.
- 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!