Click here to Skip to main content
14,209,465 members
Click here to Skip to main content
Article
Posted 28 Aug 2017

Stats

11.9K views
249 downloads
7 bookmarked

MEAN Stack with Angular 4, Auth0 Auth & JWT Authorization - Part 2

,
Rate this:
5.00 (7 votes)
Please Sign up or sign in to vote.
5.00 (7 votes)
28 Aug 2017     CPOL    
In this article, we will continue developing our MEAN stack application and add Angular 4 client for User Management views.

How to use attached code

  1. Download the attached source project. Extract it to your computer and open the folder mean-example in Visual Studio Code.
  2. In EXPLORER Panel, right click on client -> UserManagement folder and select option Open in Terminal.
  3. In terminal, run the command: npm install, wait for command completion to fully download all the packages.
  4. Next, in the same terminal, run the command: ng build to build the Angular project and save the build in folder dist.
  5. Once, client application is ready, right click on server folder in EXPLORER panel. Select option, Open in Terminal. In terminal, run the command: npm install
  6. Once the packages are successfully downloaded, edit the file server -> app.js. Update the MongoDB URL from mLab website (created in the first article).
  7. in the same terminal, run the command: node app
  8. Open the browser (Firefox or Chrome), enter the URL http://localhost:3000, your application is ready to use.

Content

Part 1: MEAN Stack, Development Environment Setup, Expressjs APIs Development

Part 2: Angular 4 Client Development.

Part 3: Authentication & Authorization using Auth0 & JWT

Introduction

In this article, we will continue developing our MEAN stack application and add Angular 4 client for User Management views. 

Background

This article is the second part of MEAN Stack with Angular 4, Auth0 Auth & JWT Authorization - Part 1 article, so please read it before starting this one. We are going to take all Angular code from Angular2 in ASP.NET MVC & Web API - Part 1 but will generate all components, service, routers, modules etc. through Angular CLI, so if you are new to Angular 2/4, I would recommend going through this article. Also, we will update our service to talk to Expressjs APIs for User Management that are pointing to MongoDB on mLab website we created in Part 1.

Visual Studio Code - Terminal

I am hoping that you have some information how to use Visual Studio Code. I just want to talk about TERMINAL tab:

Whenever in the next section, I would say right click on UserManagement or server folder and select option Open in Terminal. You would see that in a highlighted drop down it will start adding the environment where we are running these commands. So, you only need to right click on each folder once and every time when you see my statement "right click on [FOLDER] and select option Open in Terminal", you actually don't need to do it. Just select the corresponding environment from the drop down and enter the required command. E.g.. 1:cmd is for UserManagement folder and 2:node is for server folder.

Let's Start

  1. Download the attached project from MEAN Stack with Angular 4, Auth0 Auth & JWT Authorization - Part 1. Extract it, run the Visual Studio Code, go to File -> Open Folder... and open the extracted folder mean-example.

  2. Your EXPLORER panel should have two folders client and server. Right click on folder server and select Open in Terminal or Open in Command Prompt. The terminal will be opened over bottom blue ribbon in Visual Studio Code. Enter the command: npm install and press enter. 

  3. Verify the solution is working fine by following the steps in the previous article before moving further.

  4. So, as discussed earlier, I am going to take an Angular project from here. This is Angular 4 in ASP.NET MVC project, we will take most of the Angular code from here and update it where required. (Download is optional, I will provide all code in next steps)

  5. Since we will create our Angular project using Angular CLI, please go through Angular CLI command from this link or these videos.

  6. Coming back to our project in Visual Studio Code, in EXPLORER panel, right click on the folder client and select Open in Terminal or Open in Command Prompt. Enter the command ng new UserManagement --routing. It will take a few moments and generate the complete Angular 4 project with all required files and routing file. Enter the command cd UserManagement to get into the project folder. Enter the command npm install to download all packages (if you don't see folder node_modules and packages in it).

  7. Enter the command ng serve -o to build and open the application in the browser with http://localhost:4200/ URL. (use Firefox or Chrome browser)

  8. Cool, so we created the new project with routing, installed the client packages and tested it. Now let's start adding the code from here.

  9. Edit the client -> UserManagement -> src -> app -> app.component.html file and replace its content with the following code snippet: 

    <div>
     <nav class='navbar navbar-inverse'>
            <div class='container-fluid'>
                <ul class='nav navbar-nav'>
                    <li><a [routerLink]="['home']">Home</a></li>
                    <li><a [routerLink]="['user']">Users Management</a></li>
                </ul>
            </div>
        </nav>
        <div class='container'>
            <router-outlet></router-outlet>
        </div>
     </div>
    
  10. In above code, we are only adding the two links Home and User Management, and router-outlet where views would be loaded.

  11. You might have noticed, we are using the bootstrap code in the template app.component.html, so add following bootstrap CDN link in client -> UserManagement -> src -> index.html page in Head section.

    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
  12. Next, let's add the home and user components, right click on client folder and select option Open in Terminal or Open in Command Prompt (if you are using old version of Visual Studio Code).

  13. Enter the commands: ng g component home to generate the Home component.

  14. Enter the commands: ng g component user to generate the User Management component.

  15. After running the above two commands, you would see two new folders home and user in client -> UserManagement -> src -> app folder with Component, Template, CSS and Jasmine Test Script Component. That's an awesome command to make us more lazy.

  16. Let's update the routing table, edit the client -> UserManagement -> src -> app -> app-routing.module.ts file. Replace the content with the following:

    import { NgModule } from '@angular/core';
    import { Routes, RouterModule } from '@angular/router';
    import { HomeComponent } from "./home/home.component";
    import { UserComponent } from "./user/user.component";
    
    const routes: Routes = [
      { path: '', 
      redirectTo: 'home',
       pathMatch: 'full' },
      {
        path: 'home',
        component: HomeComponent,
      },
      {
        path: 'user',
        component: UserComponent,
      }
    ];
    
    @NgModule({
      imports: [RouterModule.forRoot(routes)],
      exports: [RouterModule]
    })
    export class AppRoutingModule { }
  17. We are only adding two routes for Home and User components. That's quite self-explanatory.
  18. Now, in an open terminal pointing to client -> UserManagement folder run the command ng serve -o to run the application in browser, after building the project, application would run on http://localhost:4200 URL. Remember, if your application is already running by ng serve -o, you don't need it to run this command again, after updating any file, just save it and it would be automatically updated in the browser.

  19. In the browser you should see two links Home and User Management, by clicking on Home link (defualt) you should see home works! and User Management page should have user works! text. If this is not a case, you are doing something wrong, so first fix it first and then move next.

  20. Edit the client -> UserManagement -> src -> app -> home -> home.component.html file and replace the content with the following:

    <img src="https://cdn.elegantthemes.com/blog/wp-content/uploads/2014/01/user-roles-thumb.jpg"/>
  21. Save home.component.html and go to browser, click on Home link, you would see user image.

  22. Next, we need to create the User Management component. If you already have read Angular2 in ASP.NET MVC & Web API - Part 1 article that we are kind of replicating here, you should know we are using ng2-bs3-modal third party component for modal pop up. So let's install it first, right click on client -> UserManagement folder and select Open in Terminal (Or Press Ctrl+C to exit from current process). Enter the command: npm install ng2-bs3-modal --save to install ng2-bs3-modal and save it's reference in package.json file.

  23. Add the ng2-bs3-modal package reference in client -> UserManagement -> src -> app -> app.module.ts file by replacing the content with the following:

    import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';
    
    import { AppRoutingModule } from './app-routing.module';
    import { AppComponent } from './app.component';
    import { HomeComponent } from './home/home.component';
    import { UserComponent } from './user/user.component';
    
    import { ReactiveFormsModule } from '@angular/forms';
    
    //Importing ng2-bs3-modal
    import { Ng2Bs3ModalModule } from 'ng2-bs3-modal/ng2-bs3-modal';
    
    @NgModule({
      declarations: [
        AppComponent,
        HomeComponent,
        UserComponent
      ],
      imports: [
        BrowserModule,
        AppRoutingModule,
        ReactiveFormsModule, 
        Ng2Bs3ModalModule
      ],
      providers: [],
      bootstrap: [AppComponent]
    })
    export class AppModule { }
  24. Since ng2-bs3-modal requires bootstrap and jquery as dependencies, update the client -> UserManagement -> src -> index.html file as follows:
    <!doctype html>
    <html lang="en">
    <head>
      <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
      
     <!--  For ng2-bs3-modal --> 
      <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.0/jquery.js"></script>
      <script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.js"></script>
      
      <meta charset="utf-8">
      <title>UserManagement</title>
      <base href="/">
    
      <meta name="viewport" content="width=device-width, initial-scale=1">
      <link rel="icon" type="image/x-icon" href="favicon.ico">
    </head>
    <body>
      <app-root></app-root>
    </body>
    </html>
  25. We need User model interface to hold user information, so let's run the command: ng g interface model/user. This command will create the model folder in client -> UserManagement -> src -> app and in the folder, it will generate the User interface. Update the user.ts file as follows:
    export interface IUser {
        _id: string,
        FirstName: string,
        LastName: string,
        Email: string,
        Gender: string,
        DOB: string,
        City: string,
        State: string,
        Zip: string,
        Country: string
    }
  26. In above code, I conventionally renamed the User interface to IUser. Rest of the fields are used to store user information.

  27. Next, let's create enum for database operation through command: ng g enum shared/dbop 

  28. Above command will created shared folder and dbop.enum.ts enumeration in it. Update the content as follows:

    export enum Dbop {
            create = 1,
            update = 2,
            delete =3
        }
  29. Now let's create the service that would talk to Expressjs exposed APIs to manage the user (load all users, addd, update and delete user).

  30. Right click on client -> UserManagement folder and select Open in Terminal. Run the command: ng g service service/user --module app.module.ts

  31. The above command will create the service folder in src -> app folder, create the user.service.ts in it and also add it in AppModule's providers section for dependency injection. 

  32. Edit the newly created user.service.ts file and replace its content with the following:

    import { Injectable } from '@angular/core';
    import { IUser } from "../model/user";
    import { Observable } from "rxjs/Observable";
    import { Http, Response, Headers, RequestOptions} from '@angular/http';
    import 'rxjs/add/operator/map';
    import 'rxjs/add/operator/do';
    import 'rxjs/add/operator/catch';
    
    @Injectable()
    export class UserService {
      users: IUser[];
      constructor(private _http: Http) { }
      get(): Observable<any> {
           let url="/api/users";
          return this._http.get(url)
           .map((response: Response) => <any>response.json())
           .catch(this.handleError);
      }
    
      post(model: any): Observable<any> {
        let url="/api/user";
          let body = JSON.stringify(model);
          console.log(body);
          let headers = new Headers({ 'Content-Type': 'application/json' });
          let options = new RequestOptions({ headers: headers });
          return this._http.post(url, body, options)
              .map((response: Response) => <any>response.json())
              .catch(this.handleError);
      }
    
      put(id: string, model: IUser): Observable<any> {
          let url="/api/user/"+id;
          delete model._id;
          let body = JSON.stringify(model);
          let headers = new Headers({ 'Content-Type': 'application/json' });
          let options = new RequestOptions({ headers: headers });
          //options.params.set('id',id.toString());
          return this._http.put(url, body, options)
              .map((response: Response) => <any>response.json())
              .catch(this.handleError);
      }
    
      delete(id: string): Observable<any> {
        let url="/api/user/"+id;
          let headers = new Headers({ 'Content-Type': 'application/json' });
          let options = new RequestOptions({ headers: headers });
          //options.params.set('id',id);
          return this._http.delete(url,options)
              .map((response: Response) => <any>response.json())
              .catch(this.handleError);
      }
    
      private handleError(error: Response) {
          console.error(error);
          return Observable.throw(error.json().error || 'Server error');
      }
    
    }
  33. The above code for UserService is pretty much same as we have in Angular2 in ASP.NET MVC & Web API - Part 1 article except the APIs URL, you can see the get() method URL is /api/users. When we will call Angular client from Expressjs server, this URL would be http://localhost:3000/api/users that is the URL for get all Users API we created and tested in Part 1. Same goes with POST, PUT and DELETE.

  34. Edit the client -> UserManagement -> src -> app -> user -> user.component.ts and replace its content with the following:

    import { Component, OnInit, ViewChild } from '@angular/core';
    import { ModalComponent } from "ng2-bs3-modal/ng2-bs3-modal";
    import { IUser } from "../model/user";
    import { FormBuilder, FormGroup, Validators } from '@angular/forms';
    import { Dbop } from "../shared/dbop.enum";
    import { UserService } from "../service/user.service";
    
    @Component({
      selector: 'app-user',
      templateUrl: './user.component.html',
      styleUrls: ['./user.component.css']
    })
    export class UserComponent  implements OnInit  {
       @ViewChild('modal') modal: ModalComponent;
          users: IUser[];
          user: IUser;
          msg: string;
          indLoading: boolean = false;
          userFrm: FormGroup;
          dbops: Dbop;
          modalTitle: string;
          modalBtnTitle: string;
      
          constructor(private fb: FormBuilder, private _userService: UserService) { }
      
          ngOnInit(): void {
              this.userFrm = this.fb.group({
                _id:  [''],
                FirstName:  ['',Validators.required],
                LastName:  [''],
                Email:  ['',Validators.email],
                Gender:  ['',Validators.required],
                DOB:  [''],
                City:  [''],
                State:  [''],
                Zip:  ['',Validators.required],
                Country: ['']
              });
              this.LoadUsers();
          }
      
          LoadUsers(): void {
              this.indLoading = true;
              this._userService.get()
                  .subscribe(users => { this.users = users; this.indLoading = false; },
                  error => this.msg = <any>error);
          }
      
          addUser() {
              this.dbops = Dbop.create;
              this.SetControlsState(true);
              this.modalTitle = "Add New User";
              this.modalBtnTitle = "Add";
              this.userFrm.reset();
              this.modal.open();
          }
      
          editUser(id: string) {
              this.dbops = Dbop.update;
              this.SetControlsState(true);
              this.modalTitle = "Edit User";
              this.modalBtnTitle = "Update";
              this.user = this.users.filter(x => x._id == id)[0];
              this.userFrm.setValue(this.user);
              this.modal.open();
          }
      
          deleteUser(id: string) {
              this.dbops = Dbop.delete;
              this.SetControlsState(false);
              this.modalTitle = "Confirm to Delete?";
              this.modalBtnTitle = "Delete";
              this.user = this.users.filter(x => x._id == id)[0];
              this.userFrm.setValue(this.user);
              this.modal.open();
          }
      
          onSubmit(formData: any) {
              this.msg = "";
         
              switch (this.dbops) {
                  case Dbop.create:
                      this._userService.post(formData.value).subscribe(
                          data => {
                            if (data._id != "") //Success
                              {
                                  this.msg = "Data successfully added.";
                                  this.LoadUsers();
                              }
                              else
                              {
                                  this.msg = "There is some issue in saving records, please contact to system administrator!"
                              }
                              
                              this.modal.dismiss();
                          },
                          error => {
                            this.msg = error;
                          }
                      );
                      break;
                  case Dbop.update:
                      this._userService.put(formData.value._id, formData._value).subscribe(
                          data => {
                            if (data._id != "") //Success
                              {
                                  this.msg = "Data successfully updated.";
                                  this.LoadUsers();
                              }
                              else {
                                  this.msg = "There is some issue in saving records, please contact to system administrator!"
                              }
      
                              this.modal.dismiss();
                          },
                          error => {
                              this.msg = error;
                          }
                      );
                      break;
                  case Dbop.delete:
                      this._userService.delete(formData.value._id).subscribe(
                          data => {
                            if (data._id != "") //Success
                              {
                                  this.msg = "Data successfully deleted.";
                                  this.LoadUsers();
                              }
                              else {
                                  this.msg = "There is some issue in saving records, please contact to system administrator!"
                              }
      
                              this.modal.dismiss();
                          },
                          error => {
                              this.msg = error;
                          }
                      );
                      break;
      
              }
          }
      
          SetControlsState(isEnable: boolean)
          {
              isEnable ? this.userFrm.enable() : this.userFrm.disable();
          } 
      }
  35. I just did minor changes to add more form elements for the user, rest the code is same as in Angular2 in ASP.NET MVC & Web API - Part 1 article.

  36. Edit the client -> UserManagement -> src -> app -> user -> user.component.html and replace its content with the following:

    <div class='panel panel-primary'>
      <div class='panel-heading'>
          User Management
      </div>
      <div class='panel-body'>
          <div class='table-responsive'>
              <div style="padding-bottom:10px"><button class="btn btn-primary" (click)="addUser()">Add</button></div>
              <div class="alert alert-info" role="alert" *ngIf="indLoading"><img src="https://www.smallbudgethosting.com/clients/templates/flathost/img/gears.gif" width="32" height="32" /> Loading...</div>
              <div *ngIf='users && users.length==0' class="alert alert-info" role="alert">No record found!</div>
              <table class='table table-striped' *ngIf='users && users.length'>
                  <thead>
                      <tr>
                          <th>First Name</th>
                          <th>Last Name</th>
                          <th>Gender</th>
                          <th>Email</th>
                          <th></th>
                      </tr>
                  </thead>
                  <tbody>
                      <tr *ngFor="let user of users">
                          <td>{{user.FirstName}}</td>
                          <td>{{user.LastName}}</td>
                          <td>{{user.Gender}}</td>
                          <td>{{user.Email}}</td>
                          <td>
                              <button title="Edit" class="btn btn-primary" (click)="editUser(user._id)">Edit</button>
                              <button title="Delete" class="btn btn-danger" (click)="deleteUser(user._id)">Delete</button>
                          </td>
                      </tr>
                  </tbody>
              </table>
              <div>
              </div>
          </div>
          <div *ngIf="msg" role="alert" class="alert alert-info alert-dismissible">
              <button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
              <span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>
              <span class="sr-only">Error:</span>
              {{msg}}
          </div>
      </div>
    </div>
    
    <modal #modal>
      <form novalidate (ngSubmit)="onSubmit(userFrm)" [formGroup]="userFrm">
          <modal-header [show-close]="true">
              <h4 class="modal-title">{{modalTitle}}</h4>
          </modal-header>
          <modal-body>
    
              <div class="form-group">
                  <div>
                      <span>Full name*</span>
                      <input type="text" class="form-control" placeholder="First Name" formControlName="FirstName">
                  </div>
                  <div>
                      <span>Full name</span>
                      <input type="text" class="form-control" placeholder="Last Name" formControlName="LastName">
                  </div>
                  <div>
                      <span>Gender*</span>
                      <select formControlName="Gender" class="form-control">
                          <option>Male</option>
                          <option>Female</option>
                      </select>
                  </div>
                  <div>
                    <span>Email</span>
                    <input type="text" class="form-control" placeholder="Email" formControlName="Email">
                </div>
                <div>
                    <span>Date of Birth</span>
                    <input type="date" class="form-control" placeholder="DOB" formControlName="DOB">
                </div>
                <div>
                    <span>City</span>
                    <input type="text" class="form-control" placeholder="City" formControlName="City">
                </div>
                <div>
                    <span>State</span>
                    <select formControlName="State" class="form-control">
                        <option>Virginia</option>
                        <option>New York</option>
                        <option>New Jersey</option>
                        <option>Texas</option>
                        <option>California</option>
                        <option>Delaware</option>
                    </select>
                </div>
                <div>
                    <span>Zip</span>
                    <input type="text" class="form-control" placeholder="Zip" formControlName="Zip">
                </div>
                <div>
                    <span>Country</span>
                    <select formControlName="Country" class="form-control">
                        <option>USA</option>
                        <option>Canada</option>
                    </select>
                </div>
              </div>
          </modal-body>
          <modal-footer>
              <div>
                  <a class="btn btn-default" (click)="modal.dismiss()">Cancel</a>
                  <button type="submit" [disabled]="userFrm.invalid" class="btn btn-primary">{{modalBtnTitle}}</button>
              </div>
          </modal-footer>
      </form>
    </modal>
  37. Again same code as in Angular2 in ASP.NET MVC & Web API - Part 1 article, just added few more information about User, feel free to add more columns to the list .e.g. City, State, Country etc.

  38. Cool, almost done with the Angular client application. Next step is to build the Angular application, right click on client -> UserManagement folder and select option Open in Terminal. Run the command: ng build.

  39. It will take few seconds and finally the output would be stored in client -> UserManagement -> dist folder. This is the folder we will refer in Expressjs as our target view container. This folder has index.html file that is our entry point to Angular application since it has <app-root></app-root> that is selector for AppComponent and in AppComponent, we have menu item Home and User Management (that target to corresponding view according to Routing) and <router-outlet></router-outlet> selectors where these views are rendering.

  40. OK, now let's go back to server folder, first let's create the index route in Expressjs that would point to index.html discussed in previous steps.

  41. Right click on server -> routes and select option New File. Enter file name index.js and press the Enter key.

  42. Edit the newly added index.js and add the following code:

    var express = require('express');
    var router = express.Router();
    
    router.get('/', function(req, res, next){
        res.render(path.join(__dirname, '../client/UserManagement/dist/')+'/index.html');
    });
    
    module.exports = router;
  43. So in previous step's code, we are only telling Expressjs router that if there is no route defined .i.e. http://localhost:3000. Just go to client folder and render the index.html from dist folder.

  44. Now edit the server -> app.js file and update it according to the following:

    var path = require('path');
    var bodyParser = require('body-parser');
    var users = require('./routes/user');
    //Route for index.html
    var index = require('./routes/index');
    var mongojs = require('mongojs');
    //Please use your own MongoDB URL on mLab website 
    var db = mongojs('mongodb://XXXX:XXXXXX@ds149353.mlab.com:XXXXX/userdb001', ['UserInfo']);
    
    var port = 3000;
    var app = express();
    
    app.engine('html', require('ejs').renderFile);
    app.use(express.static(path.join(__dirname, '../client/UserManagement/dist')));
    
    app.use(bodyParser.json());
    app.use(bodyParser.urlencoded({extended: false}));
    
    app.set("userdb",db);
    app.use('/', index); 
    app.use("/api",users);
    
    app.listen(port, function(){
        console.log('Server started on port '+port);
    });
  45. The basic code is explained in Part 1, here we will try to understand the new code. The first line of code we added is var index = require('./routes/index');, we are importing the index router.

  46. The next line is app.engine('html', require('ejs').renderFile);, Expressjs needs engine to render the template based on its type, template types as I discussed can be pug, html etc. Since our Angular client application is exposing its views in HTML, we are using HTML template, to read more about Express Engine, click here.

  47. In next line app.use(express.static(path.join(__dirname, '../client/UserManagement/dist')));, we are pointing to Angular build path dist to be used it's resource as static files. So this folder has index.html file, that is our entry point to Angular client application, by using the Expressjs static method, we are accessing this html file as http://localhost:3000/index.html. To read more, click here.

  48. Next line is app.use('/', index). That's tells if no route is specified .i.e. http://localhost:3000 only, serve the index router whereas in index.js we are specifying if there is HTTP GET request with "/" path means no route is specified, render the index.html from dist folder. (The Express's use method allows our application to use external routes as part of the application.)

  49. That's it for now. Right click on server folder and select option Open in Terminal. Enter the command: node app. Open the browser (Firefox or Chrome) and enter the URL: http://localhost:3000.

  50. Click on User Mangement link, check the Add, Edit and Delete functionalities and MongoDB documents on mLab. 

In Next Part

In this part, we developed the client application in Angular 4 for User Management.

In next part, we will enhance same application and implement authentication and authorization using Auth0 and JWT.

History

Created: 8/27/2017

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Yaseer Mumtaz
Software Developer (Senior)
United States United States
A senior software engineer with more than thirteen years of experience in application development. I mostly work in .NET, Angular, MEAN stack technologies and love to share what I do and learn during my day to day job. Please check my tutorials and blog:
https://fullstackhub.io
https://fullstackhubblog.com

Comments and Discussions

 
-- There are no messages in this forum --
Permalink | Advertise | Privacy | Cookies | Terms of Use | Mobile
Web04 | 2.8.190617.3 | Last Updated 28 Aug 2017
Article Copyright 2017 by Yaseer Mumtaz
Everything else Copyright © CodeProject, 1999-2019
Layout: fixed | fluid