Professional Application Development in MEAN Stack - Part 10 - B
Date Published: 11/12/2018
In Part 10 - A, we developed the login API in node.js, in this part, We will develop the client side in Angular. We will develop the Login/Logout functionality and learn how to use Angular JWT and browser local storage. The only authenticated user would be able to access the admin section after this part.

Introduction

In Part 10 - A, we developed the login API in node.js, used bcrypt to compare the hashed password and jsonwebtoken to create the public key that is embeded in successful login API response as a token. In this part, we will develop the LoginComponent and login & logout functionality.

Let's Start

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

  1. Make sure the Part 10 - A is working fine without any error. 
  2. First, let's create the login interface, in the mazharncoweb folder, right click on src -> app -> model and select an option, New File, enter the file name login.ts. Add the following code in it:  
    export interface ILogin {
        _id:string;
        UserName:string;
        Password:string
    }
    
  3. This is a very simple interface with two fields UserName and Password that will act as a model class for us. 
  4. Since we are using JSON Web Token (JWT) to avoid CSRF, we also need to angular-jwt package for some useful methods e.g. to check if a token is still valid or expired. Right click on a mazharncoweb folder and select the option, Open in Terminal, in TERMINAL tab, enter the commandnpm install @auth0/angular-jwt@^1.1.0 --save, I am using the specific version because the latest version has some conflicts and error. 
  5. Once the angular-jwt is successfully installed, we need to create the AuthService, though we will only make one POST call in this service I just want to keep it sperate from DataService because we also need to add some specific methods related to authentication. In the same TERMINAL tab, enter the command: ng g s service/auth/auth --m app.module.ts
  6. Go to src -> app -> service -> auth folder, edit the auth.service.ts file and add the following content in it:  
    import { Injectable } from '@angular/core';
    import { Http, Response, Headers, RequestOptions } from '@angular/http';
    import { environment } from "../../../environments/environment";
    import { JwtHelperService } from '@auth0/angular-jwt';
    import { ILogin } from "../../model/login";
    import { Router } from "@angular/router";
    
    @Injectable()
    export class AuthService {
    
      SERVER_URL = environment.api_url;
      jwtHelper;
    
      constructor(private _http: Http, private router: Router) {
    
        this.jwtHelper = new JwtHelperService();
      }
    
      authenticateUser(url, user) {
        let body = JSON.stringify(user);
        let headers = new Headers({ 'Content-Type': 'application/json' });
        let options = new RequestOptions({ headers: headers });
        return this._http.post(this.SERVER_URL + url, body, options)
          .map((response: Response) => <any>response.json())
      }
    
      storeUserData(token, expiresIn, user: ILogin) {
        localStorage.setItem('access_token', token);
        localStorage.setItem('expiresIn', expiresIn);
      }
    
      getToken() {
        return localStorage.getItem('access_token');
      }
    
      isloggedIn() {
        const token = this.getToken();
        return !(this.jwtHelper.isTokenExpired(token));
      }
    
      logout() {
        localStorage.clear();
        this.router.navigate(['home']);
      }
    
    }
    
  7. AuthService has five functions, let's briefly understand each:
    1. authenticateUser(): This is the main Login API function that takes the Login API URL and user object (UserName and Password), calls the API and get the response back. This response includes the successful text message and token as discussed in the last part.
    2. storeUserData(): This method stores the token and expiration time in localStorage. The localStorage is an HTML 5 feature that stores the values as a key-value pair in the browser, e.g. access_token is the key that has the token value returns from Login API. The built-in localStorage.setItem() method is used to set the value to a key.
    3. getToken(): This method returns the token by key. The built-in localStorage.getItem() method is used to get the key's value.
    4. isLoggedIn(): We are using the angular-jwt package we installed earlier to check if the token is still valid by checking its expiry time. 
    5. logout(): This method will clear everything from localStorage and redirect to home page, since for each admin page, the guard will call isLoggedIn() method, after clearing localStorage, this method will return false and would not let the user enter in the admin section. You would see it in action when we will develop the guard
  8. Next, let's create the LoginComponent, you can see Login button at a top right corner from the beginning of series. Finally, this would get activated and open the login window in a modal popup. If you are not already in TERMINAL tab pointing to a mazharncoweb folder, right click on the mazharncoweb folder and select option, Open in Terminal, enter the command: ng g c client/login
  9. Edit the src -> app -> client -> login -> login.component.css and add the following CSS in it:  
    .header{
        background: #3B5C89;
        border-bottom: 5px solid #28527F;
        height: 50px;
        padding-left: 5px;
        }
      
        .header_title{
            vertical-align:baseline;
            padding-top: 10px;
            padding-bottom: 0px;
            padding-left: 5px;
            font-size: 16pt;
            font-family: Arial, Helvetica, sans-serif;
            color: rgba(255, 255, 255, 0.85);
        }
        mat-card{
          margin: 0px;
          padding: 0px;
      
        }
        mat-card-content{
          margin: 0px;
          padding: 0px;
          width: 400px;
          height: 200px;
        }
        
      .mat-dialog-container {
          padding: 0 !important;
          margin: 0 !important;
          }
      
      .frm-ctrl {
          width: 100%;
       }
      
       .card_cntnt
       {
           padding: 20px;
       }
  10. Edit the src -> app -> client -> login -> login.component.html and add the following code in it:   
    <mat-card>
      <mat-card-header class="header">
        <mat-card-title class="header_title"><i class="material-icons">&#xE63F;</i> Admin Login</mat-card-title>
      </mat-card-header>
      <mat-card-content class="card_cntnt" >
          <div>
              <form novalidate (ngSubmit)="onSubmitLgn(lgnFrm)" [formGroup]="lgnFrm">
                <div>
                  <mat-form-field class="frm-ctrl">
                    <input matInput placeholder="Email Address" formControlName="EmailAddress">
                    <mat-error *ngIf="lgnFrm.controls['EmailAddress'].errors?.required">
                        User Name is required!
                    </mat-error>
                  </mat-form-field>
                </div>
                <div>
                  <mat-form-field class="frm-ctrl">
                    <input matInput type="password" placeholder="Password" formControlName="Password">
                    <mat-error *ngIf="lgnFrm.controls['Password'].errors?.required">
                      Password is required!
                    </mat-error>
                  </mat-form-field>
                </div>
                <div>
                  <button color="primary" type="submit" [disabled]="lgnFrm.invalid" mat-raised-button>Login</button>&nbsp;
                  <button color="warn" type="button" mat-raised-button (click)="dialogRef.close()">Cancel</button>
                </div>
              </form>
            </div>
      </mat-card-content>
    </mat-card>
  11. This is pretty straightforward HTML for LoginComponent, we have Model Driven form lgnFrm that only has two controls, EmailAddress and Password. We also specified the nice Angular Material mat-error control for required filed validation. The onSubmitLgn() is a function that will trigger when we will hit the Login button to submit the form. Since this component would open in a modal pop-up, the cancel button will hide the modal pop-up control by calling dialogRef.close() method.
  12. Edit the src -> app -> client -> login -> login.component.ts and add the following code in it:   
    import { Component, OnInit } from '@angular/core';
    import { FormGroup, FormBuilder, Validators } from "@angular/forms";
    import { AuthService } from "../../service/auth/auth.service";
    import { Util } from "../../shared/util";
    import { MatDialogRef } from "@angular/material/dialog";
    import { ResponseSnackbar } from "../../shared/enum";
    import { Router } from '@angular/router';
    
    @Component({
      selector: 'app-login',
      templateUrl: './login.component.html',
      styleUrls: ['./login.component.css']
    })
    export class LoginComponent implements OnInit {
    
      lgnFrm: FormGroup;
      LOGIN_POST_URL:string="/api/login"
    
      constructor(private _fb: FormBuilder,
                  public _authService:AuthService,
                  private _util: Util,
                  public dialogRef: MatDialogRef<LoginComponent> ,
                  private router: Router) { }
    
      ngOnInit() {
        this.lgnFrm = this._fb.group({
          EmailAddress: ['', [Validators.required, Validators.email, Validators.maxLength(250)]],
          Password: ['', [Validators.required, Validators.minLength(4), Validators.maxLength(150)]]
        });
      }
    
      onSubmitLgn(formData: any) {
         this._authService.authenticateUser(this.LOGIN_POST_URL, formData.value).subscribe(
           data => {
             if (data.success == true) //Success
             {
               this._authService.storeUserData(data.token,data.expiresIn, data.user);
               this._util.openSnackBar(data.msg, ResponseSnackbar.Sucess);
               this.dialogRef.close();
               this.router.navigate(['admin']);
             }
             else {
               this._util.openSnackBar(JSON.stringify(data.msg), ResponseSnackbar.Error);
             }
           },
           error => {
           });
      }
    }
    
  13. Let's understand the code:
    1. On the top, we have Login API URL and FormGroup object for Model Driven form.
    2. constructor(): In the constructor,
      1. We have formBuilder service parameter for creating Model Driven form,
      2. AuthService that we created earlier to use authenticateUser() and other methods,
      3. Util to open the snackbar component at the bottom for success or failure login API response,
      4. MatDialogRef to open the LoginComponent in a modal pop-up and Router to redirect to Admin section.
    3. ngOnInit(): We are defining the lgnFrm with two controls EmailAddress and Password along validations. Thanks to Angular Model Driven form built-in validation for length and email address. 
    4. onSubmitLgn(): This is login form submit function that takes the user's entered email address and password, calls the authenticateUser() function from AuthService and check the response. If user credentials are authenticated, we are calling a storeUserData() method from AuthService to store the token, expireIn (token expiration time) and user information. After that, the user is redirected to the admin home page.
  14. The LoginComponent will be open in modal pop-up control so we will explicitly add it in AppModule's entryComponents, edit the src -> app -> app.module.ts and replace its content with the following:  
    import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';
    import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
    import { FormsModule, ReactiveFormsModule } from '@angular/forms';
    //Http Module For GET, POST, PUT and DELETE RESTful APIs
    import { HttpModule } from '@angular/http';
    import { TinymceModule } from 'angular2-tinymce';
    import { EscapeHtmlPipe } from "./shared/keepHtml.pipe";
    import {
      MatAutocompleteModule,
      MatButtonModule,
      MatCardModule,
      MatCheckboxModule,
      MatDialogModule,
      MatExpansionModule,
      MatGridListModule,
      MatIconModule,
      MatInputModule,
      MatMenuModule,
      MatPaginatorModule,
      MatProgressBarModule,
      MatRadioModule,
      MatSelectModule,
      MatSlideToggleModule,
      MatSnackBarModule,
      MatSortModule,
      MatTableModule,
      MatTabsModule,
      MatTooltipModule} from '@angular/material';
    
    import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
    
    import { AppComponent } from './app.component';
    import 'hammerjs';
    
    import { AgmCoreModule } from '@agm/core';
    import { NgxCarouselModule } from 'ngx-carousel';
    
    import { HomeComponent } from './client/home/home.component';
    import { AppRoutingModule } from "./app-routing.module";
    import { ContactComponent } from './client/contact/contact.component';
    import { DataService } from './service/data/data.service';
    import { Util } from "./shared/util";
    import { ViewPageComponent } from './client/view-page/view-page.component';
    import { FooterComponent } from './footer.component';
    import { AdminComponent } from './admin/admin/admin.component';
    import { DatagridComponent } from './shared/datagrid/datagrid.component';
    import { ConfirmDeleteComponent } from './shared/messages/confirm-delete/confirm-delete.component';
    import { ManageMenuComponent } from './admin/menu/manage-menu/manage-menu.component';
    import { MenuListComponent } from './admin/menu/menu-list/menu-list.component';
    import { UserMessageComponent } from './admin/user-message/user-message.component';
    import { PageListComponent } from './admin/page/page-list/page-list.component';
    import { ManagePageComponent } from './admin/page/manage-page/manage-page.component';
    import { LoginComponent } from './client/login/login.component';
    import { AuthService } from './service/auth/auth.service';
    
    @NgModule({
      declarations: [
        AppComponent,
        HomeComponent,
        ContactComponent,
        ViewPageComponent,
        FooterComponent,
        AdminComponent,
        DatagridComponent,
        ConfirmDeleteComponent,
        ManageMenuComponent,
        MenuListComponent,
        UserMessageComponent,
        PageListComponent,
        ManagePageComponent,
        EscapeHtmlPipe,
        LoginComponent
      ],
      imports: [
        TinymceModule.withConfig({
          allow_script_urls: true,
          convert_urls: false,
          valid_elements: '*[*]',
          content_css: '/assets/css/prism.css',
          plugins: ['searchreplace', 'table', 'textpattern', 'textcolor', 'anchor', 'hr', 'emoticons', 'lists advlist', 'codesample', 'link', 'autolink', 'image', 'imagetools', 'insertdatetime',
            'fullscreen', 'media', 'template', 'code', 'preview'],
          toolbar: 'searchreplace,table,sizeselect | bold italic | fontselect |  fontsizeselect,textpattern,forecolor backcolor,anchor,hr,emoticons,bold italic underline | bullist numlist outdent indent,alignleft aligncenter alignright alignjustify,codesample,link,autolink,image,imagetools,insertdatetime,fullscreen,media,template,code,preview'
        }
        ),
        HttpClientModule,
        HttpModule,
        FormsModule,
        ReactiveFormsModule,
        AgmCoreModule.forRoot({
          apiKey: 'AIzaSyCKHGctDoGx1_YdAbRsPlJYQqlQeC6kR2E'
        }),
        NgxCarouselModule,
        BrowserModule,
        AppRoutingModule,
        BrowserAnimationsModule,
        MatButtonModule,
        MatCheckboxModule,
        MatCardModule,
        MatInputModule,
        MatRadioModule,
        MatSelectModule,
        MatTabsModule,
        MatSortModule,
        MatPaginatorModule,
        MatTableModule,
        MatSnackBarModule,
        MatIconModule,
        MatDialogModule,
        MatAutocompleteModule,
        MatGridListModule,
        MatMenuModule,
        MatProgressBarModule,
        MatExpansionModule,
        MatTooltipModule,
        MatSlideToggleModule
      ],
      providers: [DataService, Util, AuthService],
      bootstrap: [AppComponent],
      entryComponents: [ManageMenuComponent,ConfirmDeleteComponent, LoginComponent]
    })
    export class AppModule { }
    
  15. Now we have developed the LoginComponent, its time to enable the login button on the top right corner, this button is defined in AppComponent so edit the src -> app -> app.component.html and replace its content with following:  
    <div class="main_div">
      <mat-grid-list cols="10" rowHeight="2:1">
        <div>
          <mat-grid-tile>
            <div style="padding-left: 5px;">
                <a [routerLink]="['home']"> <img src='assets/images/logo1.png' class="mnc-logo" /></a>
            </div>
          </mat-grid-tile>
          <mat-grid-tile [colspan]=8>
            <div class="socail_btn_padding">
              <a target="_blank" href="https://www.facebook.com/mazhar.mahmood.16"><img src="assets/images/fb_btn1.jpg" class="socail_btn image" /></a>
              <a href="skype:plug-shop?mazharmahmood786"><img src="assets/images/skype_btn1.jpg" class="socail_btn image" /></a>
              <img src="assets/images/twtr_btn1.jpg" class="socail_btn image" />
              <img src="assets/images/lkdin_btn1.jpg" class="socail_btn image" />
            </div>
            <div style="padding-left: 100px" *ngIf="_authService.isloggedIn();">
              <img src="assets/images/admin.png" style="width:150px; height: 48px" class="lgn_btn image" (click)="redirectToAdmin()" />
            </div>
          </mat-grid-tile>
          <mat-grid-tile [colspan]=1>
            <div style="text-align: right" *ngIf="!_authService.isloggedIn(); else logout">
              <img src="assets/images/login_btn.png" class="lgn_btn image" (click)="openDialog()" />
            </div>
            <ng-template #logout>
              <div>
                <img src="assets/images/logout_btn.png" class="lgn_btn image" (click)="_authService.logout()" />
              </div>
            </ng-template>
          </mat-grid-tile>
        </div>
        <mat-grid-tile [colspan]=10 [rowspan]=3>
          <ngx-carousel [inputs]="carouselBanner" [moveToSlide]="1">
            <ngx-item NgxCarouselItem>
              <div><img src='assets/images/banner_1.jpg' width="100%" /></div>
            </ngx-item>
    
            <ngx-item NgxCarouselItem>
              <div><img src='assets/images/banner_2.jpg' width="100%" /></div>
            </ngx-item>
            <button NgxCarouselPrev>&lt;</button>
            <button NgxCarouselNext>&gt;</button>
          </ngx-carousel>
        </mat-grid-tile>
      </mat-grid-list>
      <div>
        <nav class="navbar navbar-default navbar_shadow">
          <div class="container-fluid">
            <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
              <ul class="nav navbar-nav">
                <li><a [routerLink]="['home']">Home</a></li>
                <li class="dropdown" *ngFor="let menu of mainMenus" style="cursor: pointer;">
                  <a [routerLink]="['page', menu.MenuCode]" class="dropdown-toggle" role="button" aria-haspopup="true" aria-expanded="false">{{menu.MenuName}}<span *ngIf="getChildMenu(menu.MenuCode).length>0" class="caret"></span></a>
                  <ul class="dropdown-menu" *ngIf="getChildMenu(menu.MenuCode).length>0">
                    <li *ngFor="let chldMenu of getChildMenu(menu.MenuCode)">
                      <a [routerLink]="['page', chldMenu.MenuCode]">{{chldMenu.MenuName}}</a>
                    </li>
                  </ul>
                </li>
                <li><a [routerLink]="['contact']">Contact Us</a></li>
              </ul>
              <ul class="nav navbar-nav navbar-right">
                <form class="navbar-form navbar-left" novalidate (ngSubmit)="onSubmit(searchFrm)" [formGroup]="searchFrm">
                  <div class="form-group">
                    <input type="text" class="form-control" formControlName="TextSearch" placeholder="Search">
                  </div>
                  <button type="submit" class="btn btn-default">Submit</button>
                </form>
              </ul>
            </div>
            <!-- /.navbar-collapse -->
          </div>
          <!-- /.container-fluid -->
        </nav>
      </div>
      <div class="clearfix" style="padding-top:10px; padding-bottom: 10px">
        <router-outlet></router-outlet>
      </div>
    
      <app-footer></app-footer>
    
      <br>
    </div>
  16. You can go through the login div, we are calling the AuthService's isLoggedIn() function to check if the user is not logged in (by ! operator), if Yes, we are showing the login button else we are showing the logout template's div that has logout button with a click function logout() from AuthService. Also, there would be one more button on top with the title Admin Login to redirect control to the admin home page. So pretty much we have used all methods from AuthService now
  17. Next, edit the src -> app -> app.component.ts and replace its code with the following:  
    import { Component, OnInit, ViewEncapsulation } from '@angular/core';
    import { NgxCarousel, NgxCarouselStore } from 'ngx-carousel';
    import { LoginComponent } from "./client/login/login.component";
    import { MatDialog } from "@angular/material/dialog";
    import { AuthService } from "./service/auth/auth.service";
    import { Router } from "@angular/router";
    import { DataService } from "./service/data/data.service";
    import { IMenu } from "./model/menu";
    import { FormGroup, Validators, FormBuilder } from "@angular/forms";
    
    
    @Component({
      selector: 'app-root',
      templateUrl: './app.component.html',
      styleUrls: ['./app.component.css'],
      encapsulation: ViewEncapsulation.None
    })
    
    export class AppComponent implements OnInit {
    
      GET_ALL_URL: string = "/api/menu";
      mainMenus: IMenu[];
      menus: IMenu[];
    
      searchFrm: FormGroup;
    
      public carouselBanner: NgxCarousel;
      title = 'app';
    
      constructor(private _fb: FormBuilder,
                  private dialog: MatDialog,
                  public _authService: AuthService,
                  private _dataService: DataService,
        private router: Router) { }
    
      ngOnInit() {
        this.searchFrm = this._fb.group({
          TextSearch: ['', [Validators.minLength(3)]]
        });
    
        this.carouselBanner = {
          grid: { xs: 1, sm: 1, md: 1, lg: 1, all: 0 },
          slide: 2,
          speed: 400,
          interval: 4000,
          point: {
            visible: true,
            pointStyles: `
              .ngxcarouselPoint {
                list-style-type: none;
                text-align: center;
                padding: 12px;
                margin: 0;
                white-space: nowrap;
                overflow: auto;
                position: absolute;
                width: 100%;
                bottom: 20px;
                left: 0;
                box-sizing: border-box;
              }
              .ngxcarouselPoint li {
                display: inline-block;
                border-radius: 999px;
                background: rgba(255, 255, 255, 0.55);
                padding: 5px;
                margin: 0 3px;
                transition: .4s ease all;
              }
              .ngxcarouselPoint li.active {
                  background: white;
                  width: 10px;
              }
            `
          },
          load: 2,
          loop: true,
          touch: true
        }
    
        this._dataService.get(this.GET_ALL_URL)
          .subscribe(menus => { this.menus = menus.data; this.mainMenus = this.menus.filter(x => x.MenuType == 'Main'); }
          );
    
      }
    
      getChildMenu(menuCode) {
        return this.menus.filter(x => x.ParentMenuCode == menuCode)
      }
      
      openDialog() {
        this.dialog.open(LoginComponent);
      }
    
      redirectToAdmin() {
        this.router.navigate(['/admin']);
      }
    
      onSubmit(formData: any) {
        if (this.searchFrm.valid)
          this.router.navigate(['/searchPage', formData.value.TextSearch]);
      }
    
    }
    
  18. Just look for openDialog() function, we are opening LoginComponent in a modal popup. That's the only change we need in AppComponent in order to make login button functional. 
  19. We need two more images in the asset folder. Save these two images and copy them in src -> assets -> images folder:
    1. Logout Button Image
    2. Admin Login Button Image
  20. That's it for Part 10 - B, in the upcoming part(s) we will develop the guard to protect the admin section routes, HTTP interceptor to embed the token to API requests, ErrorComponent and Search functionality.



Keywords: angular reactive form tutorial, Node.js nodeemailer, MEAN stack tutorial, Angular 5 tutorial for beginners, MEAN Stack tutorial for beginners, rxjs tutorial or beginner, rxjs vs Promise,nodemailer,pm2, node.js MongoDB, node.js mongoose configuration, angular data grid control, free angular data grid control, tinymce angular, angular text editor, angular dynamic page, multer, mime-types, save files on storage in node.js, bcrypt, json web token