14

Adding Authentication in Angular

In the previous chapter, we completed the CRUD features of our Angular application using the building blocks of NgRx. We also learned the step-by-step process of writing the actions, reducers, and effects in our application that will be used to modify the value of states. We also learned the difference between using and not using effects in the application. Effects are essential for us to communicate with the external APIs that allow the database changes to be synced in the NgRx store.

In this chapter, we will learn how to add authentication in our Angular application; we will implement a login page that will provide a valid JWT, protect routes, and apply API authentication with the use of NgRx.

In this chapter, we will cover the following topics:

  • Adding user authentication
  • Protecting routes
  • Calling an API

Technical requirements

The complete code for this chapter can be found at: https://github.com/PacktPublishing/Spring-Boot-and-Angular/tree/main/Chapter-14.

Adding user authentication

Adding user authentication is one of the main requirements in developing an application. This feature allows us to restrict pages and features from unauthorized users. We can achieve user authentication in different ways, and one way to implement this is by providing a login page that will ask for credentials.

Let’s have a look at the step-by-step process of implementing the authentication feature.

The authentication API

Let us first recap the authentication API we created in our Spring Boot project. The endpoints for authentication are as follows:

  • {BASE_URL}/authenticate: The main endpoint for authentication accepts an object with email and password fields and returns a valid JWT that will be used for calling endpoints. The following is an example response object of the endpoint:
    // valid JWT
    {
       "token": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ0ZXN0QGdtYWl sLmNvbSIsImlhdCI6MTY1OTQyODk2MSwiZXhwIjoxNjU5NDY0OTYxfQ.WU_aZjmlfw--LCovx4cZ4_hcOTGiAgPnSaM0bjdv018"
    }
  • {BASE_URL}/register: The endpoint for creating new valid credentials for login. JWT, as stated in Chapter 7, Adding Spring Boot Security with JWT, is used chiefly on RESTful web services that cannot maintain a client state since JWT holds some information connected to the user. This will be used primarily in the headers of endpoints that we will request.

In our project, let’s create a service named AuthenticateService under the core/services folder by executing the following command:

 ng g core/services/authenticate

After successfully creating the service, we will place the following code in the service:

export class AuthenticateService {
  constructor(private http: HttpClient) { }
  // for login endpoint
  login(data: {email: string, password: string}):
    Observable<any> {
    return this.http.post<any>(
     `${environment.authURL}/authenticate`,
      data).pipe(
      tap((data: any) => data),
      catchError(err => throwError(() => err))
   )
  }
  // for register endpoint
  register(data: {email: string, password: string}):
    Observable<any> {
    return this.http.post<any>(
      `${environment.authURL}/register`, data).pipe(
      tap((data: any) => data),
      catchError(err => throwError(() => err))
   )
  }
}

AuthenticateService will hold the two endpoints we will use for our login page. Now, let’s create the interceptor for our application.

The HTTP interceptor

HTTP interceptors are features of Angular that allow us to intercept HTTP requests to transform their headers, the body, or the response. This provides the intercept() function, which will enable us to get the outgoing request and call the next interceptor or the backend.

We will mainly use the interceptor to modify the headers of our endpoint requests, which will be responsible for adding the Authorization: Bearer {JWT} header for each invoked request.

To implement the interceptor, we will create the core/interceptors/header.interceptor.ts file, and we will place the following code within it:

@Injectable()
export class HeaderInterceptor implements HttpInterceptor {
  intercept(httpRequest: HttpRequest<any>, next:
    HttpHandler): Observable<HttpEvent<any>> {
    const Authorization = localStorage.getItem('token') ?
      `Bearer ${localStorage.getItem('token')}` : '';
    if(httpRequest.url.includes('api/v1'))
    return next.handle(httpRequest.clone({ setHeaders: {
      Authorization } }));
    else
    return next.handle(httpRequest);
  }
}

In the preceding code example, we have added a new implementation for the intercept() function. The first step is to retrieve the valid JWT in our local storage that will be used in the HTTP headers. We will only use the JWT if the request endpoint has an api/v1 substring, as these are the endpoints that are protected.

The next step is to clone the request and add the Authorization: Bearer {JWT} header in the cloned request and call the next() function to call the API with the added header.

We have now created our interceptor; the last step is to add the interceptor in AppModule.

Let’s have a look at the following code:

  providers: [
    { provide: HTTP_INTERCEPTORS, useClass:
               HeaderInterceptor, multi: true }
  ],

In the preceding code example, we will now intercept every HTTP call on the anti-heroes endpoint and will add the generated JWT in the request headers.

The authentication module

The next step is to create an authentication module; this module will be responsible for holding the login and registration page that will accept the users and credentials and call the authenticate and register endpoints.

To create the authentication module, we will execute the following command:

ng g m auth

After successfully creating the authentication module, we will import several modules we need for our authentication module:

@NgModule({
  declarations: [
  ],
  imports: [
    CommonModule,
    MaterialModule,
    FormsModule,
    ReactiveFormsModule,
    CoreModule,
  ]
});

Now, we will create the different parts of our module.

The authentication form

We will create the main form for our authentication module; this is considered the dumb component of our module as it will accept and emit the values of the form to the login and registration page.

To create the authentication form component, we will execute the following command:

ng g c auth/components/auth-form

After successfully creating the component, we will now implement the form’s code. In the TypeScript file of the auth-form component, we will place the following code:

export class AuthFormComponent {
  @Input() error: string = "";
  @Input() title: string = "Login"
  @Output() submitEmitter = new EventEmitter();
  form: FormGroup;
  constructor(private fb: FormBuilder) {
    this.form = this.fb.group({
      email: [''],
      password: ['']
    })
  }
  submit() {
    this.submitEmitter.emit(this.form.value);
  }
}

In the preceding code example, we can see that we have created a reactive form with an email and password form control. We have also created an emitter that will pass the values of the form into the parent component, as this component will be used by both the login and the register page. Now, we will implement the HTML code and the CSS of the auth-form component.

Note

Please refer to the link provided for the entire code implementation:

https://github.com/PacktPublishing/Spring-Boot-and-Angular/tree/main/Chapter-14/superheroes/src/app/auth/components/auth-form

In the implemented code, we have bound the reactive form with the email and password input. We have also created a condition where the button changes if the page is currently on login or register.

We have successfully created our authentication form; now, we will create our login and registration page.

The login and registration page

The login and registration pages are considered to be the smart components of our application, as these are the components that will dispatch the action for calling the authentication API.

To create the login and register page, we will execute the following command:

ng g c auth/page/login auth/page/register

After successfully creating the two pages, we will run the code for the login and register components:

Login Page

//TS File
export class LoginComponent{
  constructor(private authService: AuthenticateService,
    private router: Router) {
  }
  submit(data:{email:string, password:string}) {
    this.authService.login(data).subscribe((data) => {
      this.router.navigate(['/anti-heroes']);
      localStorage.setItem('token', data.token);
   });
  }
}
// HTML File
<app-auth-form (submitEmitter)="submit($event)"></app-auth-form>

Register Page

// TS File
export class RegisterComponent {
  error: string = "";
  constructor(private authService: AuthenticateService) {
  }
  submit(data: User) {
    this.authService.register(data).subscribe((data) => {
      this.router.navigate(['/']);
    });
  }
}
// HTML File
<app-auth-form title="Register" (submitEmitter)="submit($event)"></app-auth-form>

In the preceding code example, we can see that the login page and registration pages are using the same authentication form component. Once the form is submitted, it will pass the form value into the login() or register() functions to authenticate or create the user, respectively. If the login is successful, we will redirect the user to the anti-heroes list page and place the generated token from the API in the local storage.

The routing module

The next step is to create the auth-routing module that will define the routes for the authentication module. To create the module, let’s execute the following command:

ng g m auth/auth-routing --flat

After creating the routing module, we will run the following code:

const routes: Routes = [
  {
    path: "",
    component: LoginComponent
  },
  {
    path: "register",
    component: RegisterComponent
  }
];
@NgModule({
  declarations: [],
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule],
})
export class AuthRoutingModule {}

We also need to modify our app-routing module, as we need our base path to redirect to the login page; let’s implement the following modification:

const routes: Routes = [
  {
    path: "",
    redirectTo: "login",
    pathMatch: "full",
  },
  {
    path: "login",
    loadChildren: () =>
    import("./auth/auth.module").then((m) => m.AuthModule),
  },
  {
    path: "anti-heroes",
    loadChildren: () =>
      import("./anti-hero/anti-hero.module").then((m) =>
             m.AntiHeroModule),
  }
];

In the preceding implemented code, we can see that once we go to the base path, this will now load the AuthModule and redirect us to the login page, as shown in Figure 14.1.

Figure 14.1 – The Login page

Figure 14.1 – The Login page

We should now be able to log in with our user in the database. If no user has been created, we can create a new one using the registration page, and once the login is successful, we will be redirected to the anti-hero list page, as shown in Figure 14.2.

Figure 14.2 – The anti-hero list page

Figure 14.2 – The anti-hero list page

We can also observe that our valid JWT is already placed in our local storage as the HTTP interceptor is using the JWT. As we open a request made by our application, we can see that the headers have the generated JWT:

Accept:
application/json, text/plain, */*
Accept-Encoding:
gzip, deflate, br
Accept-Language:
en-AU,en-US;q=0.9,en;q=0.8,bs;q=0.7,fr-CA;q=0.6,fr;q=0.5,tl;q=0.4
Authorization:
Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ0ZXN0QGdtYWlsLmNvbSIsImlhdCI6MTY1OTY5ODM2NSwiZXhwIjoxNjU5NzM0MzY1fQ.2SDLmvQcME5Be9Xj-zTeRlc6kGfQVNCMIWUBOBS5afg

In the preceding example headers, we can see that the Authorization header contains the valid JWT for every API request the application calls. The placement of the JWT in the header is done when our login is successful and we are redirected to the AntiHeromodule.

Token validation

The next step we need to do is to add token validation to check whether our token already expired. To implement this feature, we add the @auth0/angular-jwt library by executing the following command:

npm install @auth0/angular-jwt --save

The @auth0/angular-jwt library provides useful functions, such as isTokenExpired(), which checks whether the JWT is expired, and decodeToken(), which retrieves the information from the JWT.

After successfully installing the library, we will add the following code to our authenticate service:

  isAuthenticated(): boolean {
    const token = localStorage.getItem('token') ?? '';
    // Check whether the token is expired and return
    // true or false
    return !this.jwtHelper.isTokenExpired(token);
  }

We also need to import the JWT module into our app.module.ts file:

 imports: [
    JwtModule.forRoot({})
  ],

We will use the isAuthenticated() function on our login and register pages to check whether a JWT is present in our local storage. If there is a valid JWT, we will redirect the application to the anti-hero list page.

Let’s have a look at the following implementation:

//login page (TS File)
constructor(private authService: AuthenticateService, private router: Router,) {
    this.checkJWT();
  }
checkJWT() {
    if(this.authService.isAuthenticated()) {
      this.router.navigate(['/anti-heroes'])
    }
  }

Logout implementation

The last feature we need to implement is the logout function. To add this feature, the only function we need to add is a function that will remove the token from our storage. Let’s have the code implementation as follows:

authenticate.service.ts

export class AuthenticateService {
… other functions
doLogout() {
    let removeToken = localStorage.removeItem('token');
    if (removeToken == null) {
      this.router.navigate(['login']);
    }
  }

In the preceding code example, we have added a doLogout() function that removes the token in the storage and redirects the application to the login page. Now, let’s edit our navbar component to have a logout button:

navbar.component.html

<p>
    <mat-toolbar color="primary">
      <span>Angular CRUD</span>
      <span class="example-spacer"></span>
      <button *ngIf="loggedIn" (click)="submit('logout')"
        mat-icon-button>
        <mat-icon>logout</mat-icon>
      </button>
    </mat-toolbar>
  </p>

navbar.component.css

.example-spacer {
    flex: 1 1 auto;
  }

navbar.component.ts

export class NavbarComponent implements OnInit{
  @Output() actionEmitter = new EventEmitter();
  @Input() loggedIn = false;
  submit(action: string) {
    this.actionEmitter.emit(action);
  }
}

In the preceding code implementation, we created an emitter for our navbar component. This will emit the action we have triggered in our navbar, and it will be passed into our app component.

The last step is to call the doLogout() function in our app component when the logout button is clicked. Let’s have a look at the code implementation, as follows:

app.component.html

<app-navbar [loggedIn]="url != '/' && !url.includes('login')" (actionEmitter)="submit($event)"></app-navbar>
<div class="container">
    <router-outlet></router-outlet>
</div>

app.component.ts

export class AppComponent {
  title = 'superheroes';
  url: string = "";
  constructor(private authService: AuthenticateService,
              private router: Router){
    this.getRoute();
  }
  submit(action: string) {
    switch (action) {
      case 'logout':
        this.authService.doLogout();
        break;
      default:
        break;
    }
  }
  getRoute() {
    this.router.events.subscribe(data => {
    if(data instanceof NavigationEnd) {
      this.url = data.url;
    }
   });
  }
}

In the preceding code implementation, we injected the authenticate service into our app component and called the doLogout() function. If the action is logout, we have also added a listener to the router change to check if our route is currently on login or register, and if it is, we will remove the logout button on the navbar component.

We have successfully implemented user authentication with our application, but we will still improve this implementation as we go on through this chapter. In the next section, we will discuss how to protect routes in our Angular application.

Protecting routes

One of the essential features of Angular is router guards. Guards are helpful if we want to protect our routes from being accessed directly without authentication or prevent the user from losing changes when navigating accidentally from the route.

Guards are interfaces provided by Angular that allow us to control the accessibility of a route with a provided condition. These are applied directly to the routes we want to protect.

Let’s have a look at some of the guards provided by Angular:

  • CanActivate: This is implemented on a route we want to prevent access to.
    • Method signature:
      canActivate(route:ActivatedRouteSnapshot, state:RouterStateSnapshot):Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree

The preceding code defines the signature of the CanActivate guard. The function accepts the ActivatedRouteSnapshot and RouterStateSnapshot parameters and returns an Observable or Promise that can be of type Boolean or UrlTree.

  • Creating the guard:
    export class AuthGuard implements CanActivate {,
    constructor(priavte auth: AuthService, private router: Router) {}
    canActivate(route: ActivatedRouteSnapshot, state:RouterStateSnapshot): Observable<boolean> |
    Promise<boolean> | boolean {
      // return true permitted in the route, else return
      // false
     }
    }

In the preceding code example, we have created a new class named AuthGuard; we have also implemented it with the CanActivate guard and added the canActivate() function for the required logic.

  • Using the guard:
    // route-module file
    { path: 'hero',
     component: HeroComponent,
     canActivate: [AuthGuard]
    }

In the preceding code example, we have used the newly created AuthGuard class in our hero route to protect it from users without a valid JWT.

  • CanActivateChild: This is similar to CanActivateGuard, but this guard is applied to prevent access to child routes. Once this is added to the parent route, the guard will protect all child routes.
    • Method signature:
      canActivateChild(route:ActivatedRouteSnapshot, state:RouterStateSnapshot):Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree

The preceding code example defines the signature of the CanActivateChild guard. The function accepts the ActivatedRouteSnapshot and RouterStateSnapshot parameters and returns an Observable or Promise that can be of type Boolean or UrlTree.

  • Creating the guard:
    export class AuthGuard implements CanActivateChild {
    constructor(private auth: AuthService, private router: Router) {}
    canActivateChild(route: ActivatedRouteSnapshot, state:RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
      // return true permitted in the route, else return
      // false
     }}

In the preceding code example, we have created a new class named AuthGuard. We have also implemented it with the CanActivateChild guard and added the canActivateChild() function for the required logic.

  • Using the guard:
    {
      path: user',
      canActivateChild: [AuthGuard],
      component: UserComponent,
      children: [
       { path: ':id', component: ProfileComponent},
       { path: ':id/edit', component: SettingsComponent}]
    }

In the preceding code example, we have used the newly created AuthGuard class in our user path to protect its child routes that navigate to the ProfileComponent and SettingsComponent components from users without a valid JWT.

  • CanLoad: This guard is used for lazy-loaded modules. The CanActivate guard can only prevent users from navigating through a route; the CanLoad guard prevents both navigating to and downloading the lazy-loaded module.
    • Method signature:
      canLoad(route:Route,segments:UrlSegment[]):Observable<boolean>|Promise<boolean>|boolean

The preceding code example defines the signature of the CanLoad guard. The function accepts the Route and UrlSegment[] parameters and returns an Observable or Promise that can be of type Boolean.

  • Creating the guard:
    import { CanLoad, Route, Router } from '@angular/router';
    export class AuthGuard implements CanLoad {
    constructor(private router: Router) {}
    canLoad(route:Route,segments:UrlSegment[]):Observable <boolean>|Promise<boolean>|boolean {
     // return true or false based on a condition to load
     // a module or not
    }}

In the preceding code example, we have created a new class named AuthGuard. We have also implemented it with the CanLoad guard and added the canLoad() function for the required logic.

  • Using the guard:
      {
        path: "hero",
        loadChildren: () =>
          import("./hero/hero.module").then((m) =>
                 m.AntiHeroModule),
          canLoad: [AuthGuard]
      }

In the preceding code example, we have used the newly created AuthGuard class in our hero route to protect it from users accessing and downloading the resources without a valid JWT.

  • CanDeactivate: This is a guard used to prevent the user from navigating away from the current route. This is useful in scenarios such as filling out forms in the application, to avoid losing some changes on navigating out accidentally.
    • Method signature:
      canDeactivate(component: T, currentRoute: ActivatedRoute Snapshot, currentState: RouterStateSnapshot,nextState?: RouterStateSnapshot): Observable<boolean|UrlTree>|Promise<boolean|UrlTree>|boolean |UrlTree;

The preceding code example defines the signature of the CanDeactivate guard. The function accepts a generic component, the ActivatedRouteSnapshot and RouterStateSnapshot parameters, and returns an Observable or Promise that can be of type Boolean or UrlTree.

  • Creating the guard:
    // CanDeactivateGuard service
    import { Observable } from 'rxjs/Observable';
    import { CanDeactivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
    export interface CanComponentDeactivate {
    canDeactivate: () => Observable<boolean> | Promise<boolean> | boolean;
    }
    export class CanDeactivateGuard implements CanDeactivate<CanComponentDeactivate> {
      canDeactivate(component:CanComponentDeactivate,current Route:ActivatedRouteSnapshot, currentState:RouterState Snapshot, nextState?: RouterStateSnapshot): Observable <boolean> | Promise<boolean> | boolean {
      return component.canDeactivate();
    }
    }

In the preceding implementation, we have created an interface that will be used in the component of the CanDeactivateGuard service:

export class FormComponent implements OnInit, CanComponentDeactivate {
canDeactivate(): Observable<boolean> | Promise<boolean> | boolean {
/* return true or false depends on a specific condition if you want to navigate away from this route or not.*/
}
}

In the preceding code example, we have implemented the interface we have created for the component.

  • Using the guard:
    { path: ':id/edit', component: FormComponent, canDeactivate: [CanDeactivateGuard] }

In the preceding code example, we have used the newly created CanDeactivateGuard to prevent the user from navigating out of the FormComponent based on the applied condition on the canDeactivate() function.

We have learned about the different guards we can use in our application. Now, let’s implement this in our Angular project.

Project implementation

The first guard we need to apply in our application is the CanLoad guard. This is necessary as we want to protect our anti-heroes routes from being accessed if there is no valid JWT. To create the CanLoad guard, execute the following command:

ng g g core/guards/auth

After executing the command, select the CanLoad option to generate a new AuthGuard class. The only thing we need to change here is the implementation of the canLoad() function. The condition we want to apply is to allow the route and modules to be loaded if the JWT is valid.

Let’s have a look at the following implementation:

@Injectable({
  providedIn: 'root'
})
export class AuthGuard implements CanLoad {
  constructor(private router: Router, private auth:
    AuthenticateService) {}
  canLoad(route: Route, segments:UrlSegment[]):
    Observable<boolean | UrlTree> | Promise<boolean |
      UrlTree> | boolean | UrlTree {
      if (!this.auth.isAuthenticated()) {
        this. router.navigate(['login']);
        return false;
      }
      return true;
}
}

In the preceding code example, we have used the isAuthenticated() function to check that the JWT is valid and not expired. If it is valid, this will return true and allow us to navigate the route. Otherwise, it will redirect us to the login page.

The last step is to apply the AuthGuard class in the anti-heroes route; in the app-routing.module.ts file, we will use the following:

const routes: Routes = [
… other routes here
  {
    path: "anti-heroes",
    loadChildren: () =>
      import("./anti-hero/anti-hero.module").then((m) =>
        m.AntiHeroModule),
      canLoad: [AuthGuard]
  }
];

We have now successfully applied the CanLoad guard in our anti-heroes route. To test whether this works, we can try deleting the token in our local storage, and this should redirect us to the login page having no valid JWT.

The last route guard we need is the CanDeactivate guard; we will apply this guard on our anti-hero form to prevent the user from losing changes when navigating away from the form. To create our CanDeactivate guard, we will execute the following command:

ng g g core/guards/form

After executing the command, select the CanDeactivate option, and this will generate a new FormGuard class. We will add an interface to this class that we will use in our form component.

Let’s have a look at the following code:

export interface CanComponentDeactivate {
  canDeactivate: () => Observable<boolean> |
    Promise<boolean> | boolean;
}
@Injectable({
  providedIn: 'root'
})
export class FormGuard implements CanDeactivate<unknown> {
  canDeactivate(
    component: CanComponentDeactivate,
    currentRoute: ActivatedRouteSnapshot,
    currentState: RouterStateSnapshot,
    nextState?: RouterStateSnapshot): Observable<boolean |
      UrlTree> | Promise<boolean | UrlTree> | boolean |
      UrlTree {
      return component.canDeactivate ?
        component.canDeactivate() : true;
  }
}

In the preceding code example, we have created the CanComponentDeactivate interface that the form component will implement. This means that the condition will be placed in the component instead of the guard. In FormComponent, we will add the following code:

export class FormComponent implements OnInit, CanComponentDeactivate {
   … other code implementation
  canDeactivate(): Observable<boolean> | Promise<boolean> |
    boolean {
    const confirmation = window.confirm('Are you sure?');
    return confirmation;
  }
   … other code implementation
}

In the preceding code example, we have implemented the FormComponent with the CanComponentDeactivate interface that we have created; we have added a window. confirm(), which will pop up a dialog box that will ask if the user wants to leave the current route. This is a simple implementation of the guard, as we can also add other conditions, such as if we only want to ask this question if there are changes in the form.

The last step is to apply the guard in the FormComponent route.

Let’s have a look at the following code:

const routes: Routes = [
 … other routes
  {
    path: "form",
    children: [
      {
        path: "",
        canDeactivate: [FormGuard],
        component: FormComponent
      },
      {
        path: ":id",
        canDeactivate: [FormGuard],
        component: FormComponent
      }
    ]
  },
];

Once we have applied the CanDeactivate guard, navigating out from the anti-hero form will pop up a dialog box for the user, as shown in Figure 14.3.

14.3 – Dialog box on navigating away from the form

We have now successfully applied guards in our Angular application; in the next section, we will directly improve our calling of API authentication with the use of NgRx state management.

Calling an API

We have already created user authentication in the previous section by calling the authentication service directly in our component. We have also stored the generated JWT in our local storage using the setItem function, which is also happening in our login component.

What we want to achieve is to reduce the responsibility of our components, and as we remember, we are using NgRx state management to call the APIs, and the only responsibility of our components is to dispatch the action, and NgRx will do the rest.

In this section, we will improve our API calls by using the building blocks of the NgRx state management.

Creating the actions

The first step we need is to create the actions for our authentication feature. We will create a file named auth.actions.ts in the auth/state folder, and we will have the following code:

import { createAction, props } from '@ngrx/store';
export enum AuthActions {
 LOGIN = '[AUTH] Login',
 SET_TOKEN = '[AUTH] Set Token',
 CREATE_USER = '[AUTH] Create User',
 AUTH_ERROR = '[AUTH] AUTH_ERROR',
}
export const setToken = createAction(
    AuthActions.SET_TOKEN,
    props<{ token: string }>(),
);
export const setError = createAction(
    AuthActions.AUTH_ERROR,
    props<{ error: any }>(),
);

In the preceding code, we can see that we have created four action types: the LOGIN type will be used for the effect responsible for calling the login API; the CREATE_USER type will be used for the effect accountable for calling the register API; the SET_TOKEN type will be used by a reducer that will set the generated JWT in the store after the login API has been reached; and lastly, the AUTH_ERROR type will be used to set errors in the store if the login or register API has returned an error.

Creating the effects

After creating our actions, now, we will create the effects for calling the login and register API. We will create a file named auth.effects.ts in the auth/state folder, and we will have the following implementation:

Login Effect

@Injectable()
  loginUser$ = createEffect(() => {
    return this.actions$.pipe(
        ofType(AuthActions.LOGIN),
        mergeMap(((data: {type: string, payload: User}) =>
          this.authService.login(data.payload)
          .pipe(
            map(data => ({ type: AuthActions.SET_TOKEN,
                           token: data.token })),
            tap(() =>
              this.router.navigate(["anti-heroes"])),
            catchError(async (data) => ({ type:
              AuthActions.AUTH_ERROR, error: data.error }))
          ))
        ))
    }, {dispatch: true}

Register Effect

  createUser$ = createEffect(() => {
    return this.actions$.pipe(
        ofType(AuthActions.CREATE_USER),
        mergeMap(((data: {type: string, payload: User}) =>
          this.authService.register(data.payload)
          .pipe(
            tap(() =>  this.router.navigate(["login"])),
            catchError(async (data) => ({ type:
              AuthActions.AUTH_ERROR, error: data.error }))
          ))
        ))
    }, {dispatch: true}
  );

In the preceding code, we have created effects for the login and register API. In the loginUser$ effect, once the login API is successful, it will dispatch the SET_TOKEN action and pass the generated JWT, and this will also redirect us to the anti-heroes page.

This is the same behavior we implemented in the previous section. On the other hand, the createUser$ effect, once the register API is successful, will redirect us to the login page again. This is a simple behavior, and you can customize what will happen next if the registration is successful.

We have also implemented the AUTH_ERROR action, which will be called when the login or register API fails.

Creating the reducers

The next step we need is to create the reducers. We will create a file named auth.reducers.ts in the auth/state folder, and we will have the following implementation:

export interface AuthState {
    token: string;
    error: any
}
export const initialState: AuthState = {
    token: "",
    error: null
}
export const authReducer = createReducer(
  initialState,
  on(setToken, (state, { token }) => { return {...state,
     token}}),
  on(setError, (state, { error }) => { return {...state,
     error}}),
  );

In the preceding code example, we can see that AuthState has two fields, which are token and error. The token field will contain the valid JWT once the setToken action is called when the authentication API is successful, and the error field will contain the generated error if the login or register API fails.

Creating the selectors

After creating the reducers, we will now create our selector. In this case, our selector will be simple as we only need a selector for the error field. We will create a file named auth.selectors.ts in the auth/state folder, and we will have the following implementation:

import { createSelector, createFeatureSelector } from '@ngrx/store';
import { AppState } from 'src/app/state/app.state';
import { AuthState } from './auth.reducers';
export const selectAuthState = createFeatureSelector<AuthState>('authState')
export const selectError = () => createSelector(
    selectAuthState,
    (state: AuthState) => state.error
)

In the preceding code example, we have created a selector for our error field; we will need this selector to display the error message in our component for the user.

Syncing in local storage

The next feature we will implement is the syncing of our state in local storage. We can achieve this by using localStorage.setItem() in our application. However, using this will not be maintainable, and the setting of values in the storage will be in different places.

To have a better implementation, we will use the ngrx-store-localstorage library. To install the library, we will execute the following command:

npm install ngrx-store-localstorage --save

After successfully installing the library, we should determine the states we want to sync with our local storage. In our case, we want the token field in our auth state to be synced. To achieve this, we make the following code changes in auth.module.ts:

import { localStorageSync } from 'ngrx-store-localstorage';
export function localStorageSyncReducer(reducer: ActionReducer<any>): ActionReducer<any> {
  return localStorageSync({keys: ['token']})(reducer);
}
const metaReducers: Array<MetaReducer<any, any>> = [localStorageSyncReducer];
@NgModule({
  declarations: [
   … declared components
  ],
  imports: [
   … other imported modules
    StoreModule.forFeature('authState', authReducer,
     {metaReducers}),
    EffectsModule.forFeature([AuthEffects]),
  ]
})

In the preceding code, we can see that we have created a dedicated reducer that calls the localStorageSync from the ngrx-store-localstorage, which is responsible for adding values in the local storage.

We can also specify what fields we want to sync and, in this case, we have added the token in the keys array. Once the token state changes its value, the new value will also be placed in our storage.

Dispatching and selecting a component

The last step is to dispatch the actions and use the selector for our login and register a component. Let’s have a look at the following code implementation for the login and register a component:

login.component.ts

export class LoginComponent{
  error$ = this.store.select(selectError());
  errorSub: Subscription | undefined;
  constructor(private store: Store, private authService:
    AuthenticateService, private router: Router,
      private _snackBar: MatSnackBar) {
    this.checkJWT();
    this.getError();
  }
  submit(data: User) {
    this.store.dispatch({type: AuthActions.LOGIN,
                         payload: data})
  }
  ngOnDestroy(): void {
    this.errorSub?.unsubscribe();
  }
  getError() {
    this.error$.subscribe(data => {
      if(data) {
        this._snackBar.open(data.message, "Error");
      }
    })
  }
… other code implementation
}

register.component.ts

export class RegisterComponent implements OnInit, OnDestroy {
  error$ = this.store.select(selectError());
  errorSub: Subscription | undefined;
  constructor(private store: Store,  private _snackBar:
              MatSnackBar) {
    this.getError();
  }
  ngOnDestroy(): void {
    this.errorSub?.unsubscribe();
  }
  submit(data: User) {
    this.store.dispatch({type: AuthActions.CREATE_USER,
                         payload: data})
  }
  getError() {
    this.errorSub = this.error$.subscribe(data => {
       if(data) {
         this._snackBar.open(data.message, "Error");
       }
     })
   }
… other code implementation
}

We can see in the preceding code the login and register pages have almost the same implementation. We have already removed the call for the login and register service in the submit function and replaced it with the dispatching of an action. We have also used the selectError() selector to listen to see if the APIs have produced errors.

Summary

With this, we have reached the end of this chapter; let’s have a recap of the valuable things you have learned.

We now know how to implement user authentication in the Angular application, and we have used an HTTP interceptor to intercept HTTP requests to transform its headers and add the valid JWT for the API calls. We have also learned about the different route guards that allow us to protect routes from unauthorized access or prevent accidental loss of data when navigating out from the route. Lastly, we have learned how to use NgRx state management by improving how to implement authentication in Angular.

The next chapter will teach us how to write end-to-end testing in Angular using the Cypress framework.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.141.193.158