4.4.2 Develop frontend authentication: Implementing Cookie-Based Authentication and Role-Based Access Control in Angular: AuthService, Guards, Interceptors, and Navigation

In previous article we updated our backend project to add Role Based Access Control, configured CORS in backend’s main.ts file and enhanced our logout feature. Now, we are ready back to the fronted app to develop frontend authentication.

auth-service library

Generate auth-service library:

npx nx g @nx/angular:library auth \\
--prefix mtfs \\
--importPath=@mtfs/frontend/auth-service \\
--tags type:feature,scope:frontend \\
--directory=libs/frontend/auth/auth-service \\
--projectNameAndRootFormat=as-provided 

Generate AuthService class in auth library:

npx nx generate @schematics/angular:service --name=auth \\
--project=auth \\
--path=libs/frontend/auth/auth-service/src/lib/ \\
--no-interactive

In the AuthService class, it is necessary to implement functionality that interacts with the backend’s ‘auth’ API endpoints and performs the functions of registering a user, logging in a user, logging out a user, updating a user, and retrieving a user’s profile.

Before update AuthService class we add ILogin interface to libs/shared/domain library:

export interface ILogin {
  email: string;
  password: string;
}

And we create create an httpRequestInterceptor to centrally manage the base URL of HTTP requests:

First, we create class APISettings to setup our base API endpoint:

import { HttpHeaders } from "@angular/common/http";

export class APISettings{
  public static API_ENDPOINT = '<http://localhost:3000/api>';
  public static httpOptions = {
    headers: new HttpHeaders({'Content-Type': 'application/json'})
  }
}

Second, generate httpRequestInterceptor:

npx nx generate @schematics/angular:interceptor \\
--name=http-reuqest \\
--project=frontend \\
--path=libs/frontend/utils/src/lib/inrterceptors \\
--no-interactive

Update generated httpRequestInterceptor:

import { HttpInterceptorFn } from '@angular/common/http';
import { APISettings } from '../settings/api.settings';

export const httpRequestInterceptor: HttpInterceptorFn = (request, next) => {
  const modifiedRequest = request.clone({
    url: `${APISettings.API_ENDPOINT}${request.url}`,
  });
  const finalRequest = modifiedRequest.clone(APISettings.httpOptions);  
  return next(finalRequest);
};

And we will use this interceptor in AuthService class

Update apps/frontend/src/app/app.config.ts:

import { ApplicationConfig, importProvidersFrom } from '@angular/core';
import { RouterModule, provideRouter, withEnabledBlockingInitialNavigation } from '@angular/router';
import { appRoutes } from './app.routes';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule, NoopAnimationsModule } from '@angular/platform-browser/animations';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { httpRequestInterceptor} from '@mtfs/frontend/utils'

export const appConfig: ApplicationConfig = {
  providers: [
    importProvidersFrom(
      BrowserModule, 
      BrowserAnimationsModule, 
      NoopAnimationsModule, 
      RouterModule,
      ),
    provideRouter(appRoutes, withEnabledBlockingInitialNavigation()),
    provideHttpClient(withInterceptors([httpRequestInterceptor])),
  ],  
};

Update AuthService class:

import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable, map } from 'rxjs';
import { ICreateUser, ILogin, IUser } from '@mtfs/shared/domain';
import { HttpClient,  } from '@angular/common/http';

const authApi = '/auth'

@Injectable({
  providedIn: 'root'
})
export class AuthService {
  private userSubject: BehaviorSubject<IUser | null>;
  private user: Observable<IUser | null>;

  constructor(
    private http: HttpClient
  ) {    
    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
    this.userSubject= new BehaviorSubject(JSON.parse(localStorage.getItem('user')!));
    this.user = this.userSubject.asObservable();
  }

  public get userValue(){
    return this.userSubject.value;
  }

  register(user: ICreateUser) {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    return this.http.post<any>(`${authApi}/register`, user, )
      .pipe(map(user => {
        localStorage.setItem('user', JSON.stringify(user));
        this.userSubject.next(user);
        return user;
      }));
  }

  login(login: ILogin){
    return this.http.post<IUser>(`${authApi}/login`, login)
      .pipe(map(user => {
        localStorage.removeItem('user');
        localStorage.setItem('user', JSON.stringify(user));
        this.userSubject.next(user);
        return user;
      }));
  }

  logout(): Observable<void>{
    this.userSubject.next(null);
    localStorage.removeItem('user');
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    return this.http.get<any>(`${authApi}/logout`);
  }

  refresh() {
    return this.http.post<IUser>(`${authApi}/refresh`, {})
      .pipe(map(user => {
        localStorage.setItem('user', JSON.stringify(user));
        this.userSubject.next(user);
        return user;
      }))
  }
}

Logout feature in HeaderComponent

update logout method in libs/frontend/ui/ui-components/src/lib/header/header.component.ts:


  logout(){
    this.authService.logout()
      .subscribe({
        // eslint-disable-next-line @typescript-eslint/no-empty-function
        next: ()=>{
        },
        error: (error)=>{
          return throwError(()=> error);
        }
      })
      this.router.navigate(['auth/login']);   
  }

auth-ui library

Generate auth-ui library:

npx nx g @nx/angular:library auth-ui \\
--prefix mtfs \\
--importPath=@mtfs/frontend/auth-ui \\
--tags type:ui,scope:frontend \\
--directory=libs/frontend/auth/auth-ui \\
--routing \\
--projectNameAndRootFormat=as-provided 

Delete default component and generate register and login components

npx nx g @nx/angular:component register \\
--prefix mtfs \\
--directory=libs/frontend/auth/auth-ui/src/lib/register \\
--style=scss \\
--nameAndDirectoryFormat=as-provided
npx nx g @nx/angular:component login \\
--prefix mtfs \\
--directory=libs/frontend/auth/auth-ui/src/lib/login \\
--style=scss \\
--nameAndDirectoryFormat=as-provided

Update generated Register component:

import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import {FormBuilder, FormGroup, Validators, ReactiveFormsModule, FormsModule  } from '@angular/forms'
import { ActivatedRoute, Router } from '@angular/router';
import {MatCardModule} from '@angular/material/card';
import {MatFormFieldModule} from '@angular/material/form-field';
import {MatInputModule} from '@angular/material/input';
import {MatButtonModule} from '@angular/material/button';

import { AuthService } from '@mtfs/frontend/auth-service';
import { ICreateUser } from '@mtfs/shared/domain';

@Component({
  selector: 'mtfs-register',
  standalone: true,
  imports: [CommonModule],
  templateUrl: './register.component.html',
  styleUrl: './register.component.scss',
})
export class RegisterComponent implements OnInit{
  currentForm!: FormGroup;
  loading = false;
  submitted = false;
  numericPattern = '[0-9]*';
  error= '';
  errorMessage='';
  excistedEmail = false;

  constructor(
    private authService: AuthService,
    private route: ActivatedRoute,
    private router: Router,
    private formBuilder: FormBuilder
  ){}

  createForm(){
    this.currentForm = this.formBuilder.group({
      firstname: ['', Validators.required],
      lastname: ['', Validators.required],
      phoneNumber: ['', [Validators.minLength(10), Validators.maxLength(14), Validators.required, Validators.pattern(this.numericPattern),]],      
      email: ['', Validators.compose([Validators.required, Validators.email])],
      password: ['', Validators.required]
    })   }

  ngOnInit(): void{    
    this.createForm();
  }

  get emailControl(){
    return this.currentForm.get('email');
  }

  onSubmit(){
    this.submitted= true;
    if(this.currentForm.invalid) {
      return}
    this.loading=true;
    const user: ICreateUser = {
      email: this.currentForm.value.email,
      phoneNumber: "+" + this.currentForm.value.phoneNumber,      
      password: this.currentForm.value.password,
      firstname: this.currentForm.value.firstname,
      lastname: this.currentForm.value.lastname,
    }

    this.authService.register(user)
    .subscribe({
      next: ()=> {
        const returnUrl = this.route.snapshot          
          .queryParams['returnUrl'] || '/';
          this.router.navigateByUrl(returnUrl);            
      },
      error: error => {

        this.loading = false;
        if (error.status ===409){
          this.currentForm.controls['email'].setErrors({"conflict": true});
          this.currentForm.invalid;
          this.errorMessage = error.error.message;
        } else if (error.status ===400){    
          this.currentForm.controls['phoneNumber'].setErrors({"invalid-format": true});      
          this.currentForm.invalid;
          this.errorMessage = error.error.message;
        }
        else {
          this.errorMessage = 'Unexpected error occurred';
        }
      }
    });    
  }

  onLoginForm(){
    this.router.navigate(['/auth/login']);
  }  
}

Update generated Register component template:

<div class = "card-container">
  <mat-card class="card">
    <mat-card-header>
      <mat-card-title>
        Enter your register data
      </mat-card-title>
      <mat-card-subtitle>

      </mat-card-subtitle>
    </mat-card-header>
    <mat-card-content class="container">
      <form [formGroup] = "currentForm" (ngSubmit)="onSubmit()">
      
      <!--First name-->
      <mat-form-field>
        <mat-label>First name</mat-label>
        <input matInput type="text" placeholder="First name" formControlName = "firstname" #firstname required>
        @if(currentForm.controls['firstname'].hasError('required')){
          <mat-error>Required</mat-error>
        }
      </mat-form-field>

      <!--Last name-->
      <mat-form-field>
        <mat-label>Last name</mat-label>
        <input matInput type="text" placeholder="Last name" formControlName = "lastname" #lastname required>
        @if(currentForm.controls['lastname'].hasError('required')){
          <mat-error>Required</mat-error>
        }
      </mat-form-field>

      <!--Email-->
      <mat-form-field >
        <mat-label>Email</mat-label>
        <input matInput type="text" placeholder="Email" formControlName = "email" #email required>
        @if(currentForm.controls['email'].hasError('required')){
          <mat-error>Required</mat-error>
        }
        @if(currentForm.controls['email'].hasError('email')
        && !currentForm.controls['email'].hasError('required')){
          <mat-error>Enter valid email addres</mat-error>
        }
        @if(currentForm.controls['email'].hasError('conflict')){
          <mat-error>{{this.errorMessage}} Enter another email or do login.</mat-error>
        }
      </mat-form-field>

      <!--Phone number-->
      <mat-form-field>
        <mat-label>Phone number</mat-label>
        <span matPrefix>+</span>
        <input 
          matInput type="tel" 
          placeholder="Phone number" 
          formControlName = "phoneNumber" 
          #phoneNumber
          minlength="10"
          maxlength="14"
          required>
        
        @if(currentForm.controls['phoneNumber'].hasError('required')){
          <mat-error>Required</mat-error>
        }
        @if(currentForm.controls['phoneNumber'].hasError('minlength')){
          <mat-error>Min count of numbers is 10</mat-error>
        }
        @if(currentForm.controls['phoneNumber'].hasError('pattern')){
          <mat-error>Enter only numbers</mat-error>
        }
        @if(currentForm.controls['phoneNumber'].hasError('invalid-format')){
          <mat-error>{{this.errorMessage}}</mat-error>
        }
      </mat-form-field>

      <!--Password-->
      <mat-form-field>
        <mat-label>Password</mat-label>
        <input matInput type="password"  placeholder="Password" formControlName ="password" autocomplete="off" #password required>
        @if(currentForm.controls['password'].hasError('required')){
          <mat-error>Required</mat-error>
        }         
      </mat-form-field> 

      <mat-card-actions class="card-actions">        
        <button mat-stroked-button class="basic-button" color="primary" type="sybmit" [disabled]="loading">Register</button>       
        <button mat-stroked-button class="basic-button" color="primary" type="button" (click)="onLoginForm()">Login</button>
      </mat-card-actions>
      </form>
    </mat-card-content>
  </mat-card>
</div>

Update generated Login component:

import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { 
  FormBuilder, 
  FormGroup, 
  Validators, 
  ReactiveFormsModule, 
  FormsModule
  } from '@angular/forms'
import { ActivatedRoute, Router } from '@angular/router';
import {MatCardModule} from '@angular/material/card';
import {MatFormFieldModule} from '@angular/material/form-field';
import {MatInputModule} from '@angular/material/input';
import { MatButtonModule } from '@angular/material/button';

import { AuthService } from '@mtfs/frontend/auth-service';
import {  ILogin } from '@mtfs/shared/domain';
import { first } from 'rxjs';

@Component({
  selector: 'mtfs-login',
  standalone: true,
  imports: [
    CommonModule,
    FormsModule,
    MatCardModule,
    MatFormFieldModule,
    ReactiveFormsModule,
    MatInputModule,
    MatButtonModule
  ],
  templateUrl: './login.component.html',
  styleUrl: './login.component.scss',
})
export class LoginComponent {

  login!: ILogin;
  loginForm!: FormGroup;
  loading = false;
  submitted = false;
  error = '';
  errorMessage = '';

  constructor(
    private formBuilder: FormBuilder,    
    private route: ActivatedRoute,
    private router: Router,
    private authService: AuthService,
    
  ){
    //redirect to home page if user already logged in
    if (this.authService.userValue){
      this.router.navigate(['/']);
    }
  }
  createForm(){
    this.loginForm = this.formBuilder.group({      
      email: ['', [Validators.required, Validators.email]],
      password: ['', Validators.required]
    })
  }

  ngOnInit(){
    this.createForm();
  }

  onSubmit(){
    this.submitted = true;
    if(this.loginForm.invalid) return;
    this.login = this.loginForm.value
    this.error = '';
    this.loading = true;    
    this.authService.login(this.login)
      .pipe(first())
      .subscribe({
        next: ()=> { 
          const returnUrl = this.route.snapshot
            .queryParams['returnUrl'] || '/';
            this.router.navigateByUrl(returnUrl); 
        },
        error: (error) => {      
          this.error=error;
          this.errorMessage= error.error.message;
            if(error.status ===404){
              this.loginForm.controls['email'].setErrors({"notFound":true});         
              
            }else if(error.status ===401){
              this.loginForm.controls['password'].setErrors({"unauthorized":true});
            } else {
              this.loginForm.setErrors({"other": true});
              this.errorMessage='Something went wrong! Please try again later.'
            }            
          this.loading = false;
        }}
      )
  }
  onRegisterFrom(){
    this.router.navigate(['/auth/register']);
  }
}

Update generated Login component template:

<div class = "card-container">
  <mat-card class="card">
    <mat-card-header>
      <mat-card-title >Enter your email and password</mat-card-title>
    </mat-card-header>
    <mat-card-content>        
      <form  [formGroup]="loginForm" (ngSubmit)="onSubmit()">                  
        <mat-form-field>
          <mat-label>Email</mat-label>
          <input
            matInput
            type="text"
            placeholder="email"
            formControlName="email"         
            required> 
          @if(loginForm.controls['email'].hasError('required')){
            <mat-error> Email is <strong>required</strong> </mat-error>
          }            
          @if(!loginForm.controls['email'].hasError('required')
            && loginForm.controls['email'].hasError('email')){
            <mat-error>Enter valid email address</mat-error>
          }
          @if(loginForm.controls['email'].hasError('notFound')){
          <mat-error> {{this.errorMessage}} </mat-error>
          }
        </mat-form-field>

        <mat-form-field>
          <mat-label>Password</mat-label>
          <input
            matInput
            type="password"
            placeholder="password"
            formControlName="password"
            autocomplete="on"
            required>
          @if(loginForm.controls['password'].hasError('required')){
            <mat-error> Password is <strong>required</strong></mat-error>
          }            
          @if(loginForm.controls['password'].hasError('unauthorized')
            && !loginForm.controls['password'].hasError('required')){
            <mat-error>{{this.errorMessage}}</mat-error>
          } 
        </mat-form-field>

        @if(loginForm.hasError('other')){
          <span [ngStyle]="{'color':'red', 'margin-left':'2px'}">{{this.errorMessage}}</span> 
        }
        <mat-card-actions class="card-actions">
          <button mat-stroked-button class="basic-button" color="primary" type="sybmit" [disabled]="loading">Login</button>
          <button  mat-stroked-button class="basic-button" color="primary" type="button" (click)="onRegisterFrom()">Register</button>
        </mat-card-actions>
      </form>
    </mat-card-content>

  </mat-card>
</div>

Add styles for controls of login and register forms:

card (libs/frontend/ui/ui-style/src/lib/scss/components/_card.scss):

.card-container {
  position: relative;
  top: 85px;
}

.card {
  max-width: 400px;
  width: 40%;
  height: auto;
  margin: auto; 
}

.mat-mdc-card-title{
  padding-bottom: 10px; 
  font-size: 24px; 
}

.card-actions{
  padding: 0px!important;
}

field (libs/frontend/ui/ui-style/src/lib/scss/components/field.scss):

mat-form-field {
  width: 100%;
}
::ng-deep   .mat-mdc-form-field-bottom-align{
  min-height: 16px !important;
}

button (libs/frontend/ui/ui-style/src/lib/scss/components/_button.scss)

.basic-button{
  margin: 8px 8px 8px 8px;
  position: relative;
  font-size: 16px !important; 
}

in Auth library rename file lib.routes.ts to auth.routes.ts and update this file:

import { Route } from '@angular/router';
import { RegisterComponent } from './components/register/register.component';
import { LoginComponent } from './components/login/login.component';

export const authRoutes: Route[] = [
  {
    path: '', redirectTo: '/auth/login', pathMatch: 'full'
  },
  {
     path: 'register', component: RegisterComponent 
  },
  {
    path: 'login', component: LoginComponent 
  },
];

Update apps/frontend/src/app/app.routes.ts in frontend app to add authRoutes to appRoutes:

import { Route } from '@angular/router';

export const appRoutes: Route[] = [
  {
    path: 'auth',
     loadChildren: () =>  import('@mtfs/frontend/auth').then((auth) => auth.authRoutes),

  },
  {
    path: '',
    async loadComponent() {
      const c = await import('@mtfs/frontend/ui-components');
      return c.HomeComponent
    }
  }
];

Add angular provideHttpClient() to apps/frontend/src/app/app.config.ts


export const appConfig: ApplicationConfig = {
  providers: [
    importProvidersFrom(
      BrowserModule, 
      BrowserAnimationsModule, 
      NoopAnimationsModule, 
      RouterModule,
      ),
    provideRouter(appRoutes, withEnabledBlockingInitialNavigation()),
    
  ],  
};

auth-guards library

Generate auth-guards library:

npx nx g @nx/angular:library auth-guards \\
--prefix mtfs \\
--importPath=@mtfs/frontend/auth-guards \\
--tags type:feature,scope:frontend \\
--directory=libs/frontend/auth/auth-guards \\
--projectNameAndRootFormat=as-provided 
Generate authGuard:

npx nx generate @schematics/angular:guard \\
--name=auth \\
--project=frontend \\
--path=libs/frontend/auth/auth-guards/src/lib \\

update authGuard with following code:

import { inject } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivateFn, Router, RouterStateSnapshot } from '@angular/router';

import { AuthService } from '@mtfs/frontend/auth-service';

export const authGuard: CanActivateFn = (
  route: ActivatedRouteSnapshot, 
  state: RouterStateSnapshot ) => {
    const router: Router = inject(Router);
    const authService: AuthService = inject(AuthService);
    const user = authService.userValue; 
    if (user){
      const allowedRouteRoles = route.data?.['userRole'];
      if (!allowedRouteRoles){
        //allowed access
        return true;
      }
      if (allowedRouteRoles && !allowedRouteRoles.some((roleName: string)=>user.role?.name.includes(roleName))){    
          //forbidden access
          router.navigate(['/403']);  
          return false;        
      }      
      //allowed access
      return true;       
    } else {
      //user not authenticated
      router.navigate(['/auth/login'], {queryParams: {returnUrl: state.url}});
      return false;
    }
};

auth-interceptor library

Generate auth-interceptors library:

npx nx g @nx/angular:library auth-interceptors \\
--prefix mtfs \\
--importPath=@mtfs/frontend/auth-interceptor \\
--tags type:feature,scope:frontend \\
--directory=libs/frontend/auth/auth-interceptors \\
--projectNameAndRootFormat=as-provided 

Generate interceptor:

npx nx generate @schematics/angular:interceptor \\
--name=auth \\
--project=frontend \\
--path=libs/frontend/auth/auth-interceptors/src/lib

Update the generated Auth Interceptor with the following code:

import { HttpErrorResponse, HttpEvent, HttpInterceptorFn } from '@angular/common/http';
import { Observable, catchError, switchMap, throwError } from 'rxjs';
import { Router } from '@angular/router';
import { inject } from '@angular/core';
import { AuthService } from '@mtfs/frontend/auth-service';

export const authInterceptor: HttpInterceptorFn = (req, next) => {
  let isRefreshing = false;
  const router: Router = inject(Router)
  const authService: AuthService = inject(AuthService);
  req = req.clone({
    withCredentials: true,
  });
  
  if((req.url.toLowerCase().includes('/login')) ||
    (req.url.toLowerCase().includes('/logout')) ||
    (req.url.toLowerCase().includes('/register')) ||
    (req.url.toLowerCase().includes('/refresh')))
    {  
      return next(req);
    } else {
      return next(req).pipe(
        
        catchError((error: HttpErrorResponse) => {
          if(error && error.status === 401 ){
            //401 Unauthorized
            if(!isRefreshing){
              isRefreshing = true;
              const user = authService.userValue;
              if(user){
                //try to refresh token
                return authService.refresh().pipe(                  
                  switchMap(() => {
                    isRefreshing= false;
                    return next(req);
                  }),
                  catchError((originalError)=> {
                    isRefreshing= false;
                    authService.logout();
                    return throwError(() => originalError)
                  })
                )                
              } else {
                isRefreshing = false;
                return next(req)
              }
            } else {
              return next(req)
            }
          } else if(error.status === 403){
            //403 - Forbidden - move to access denied page
            router.navigate(['/403']);
            return throwError(() => error);
          } else {
            //some another server error, move to server error page
            router.navigate(['server-error']);
            return throwError(() => error);
          }
        })
      ) as Observable<HttpEvent<unknown>>;
    }   
};

This interceptor solves the following tasks:

  1. Passing cookies with authentication credentials to the server with the request:
req = req.clone({
  withCredentials: true,
});
  1. Skipping some URLs for authentication:
if ((req.url.toLowerCase().includes('/login')) ||
    (req.url.toLowerCase().includes('/logout')) ||
    (req.url.toLowerCase().includes('/register')) ||
    (req.url.toLowerCase().includes('/refresh'))) {  
  return next(req);
}
  1. Error handling of HTTP responses and errors:
      return next(req).pipe(
        
        catchError((error: HttpErrorResponse) => {
          if(error && error.status === 401 ){
            //401 Unauthorized
            if(!isRefreshing){
              isRefreshing = true;
              const user = authService.userValue;
              if(user){
                //try to refresh token
                return authService.refresh().pipe(                  
                  switchMap(() => {
                    isRefreshing= false;
                    return next(req);
                  }),
                  catchError((originalError)=> {
                    isRefreshing= false;
                    authService.logout();
                    return throwError(() => originalError)
                  })
                )                
              } else {
                isRefreshing = false;
                return next(req)
              }
            } else {
              return next(req)
            }
          } else if(error.status === 403){
            //403 - Forbidden - move to access denied page
            router.navigate(['/403']);
            return throwError(() => error);
          } else {
            //some another server error, move to server error page
            router.navigate(['server-error']);
            return throwError(() => error);
          }
        })
      ) as Observable<HttpEvent<unknown>>;
    } 

Update apps/frontend/src/app/app.config.ts by adding authInterceptor to provideHttpClient

provideHttpClient(withInterceptors([httpRequestInterceptor, authInterceptor])),

Create Role Based navigation

update libs/frontend/ui/ui-components/src/lib/home/home.component.ts to get current user role:

import { Component, OnInit, ViewChild } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { MatSidenavModule} from '@angular/material/sidenav';
import { MatSidenav } from '@angular/material/sidenav';
import { MatListModule } from '@angular/material/list';
import { HeaderComponent } from '../header/header.component';

import { SideNavService } from '@mtfs/frontend/utils';
import { AuthService } from '@mtfs/frontend/auth-service';
import { NavigationAdminComponent } from '../navigation-admin/navigation-admin.component';
import { NavigationUserComponent } from '../navigation-user/navigation-user.component';

@Component({
  selector: 'mtfs-home',
  standalone: true,
  imports: [
    CommonModule,
    RouterModule,
    MatSidenavModule,
    MatListModule,
    HeaderComponent,
    NavigationAdminComponent,
    NavigationUserComponent
  ],
  templateUrl: './home.component.html',
  styleUrl: './home.component.scss',
})
export class HomeComponent implements OnInit{
  
  @ViewChild('sidenav', {static:true}) public sidenav!: MatSidenav;
  roleName!: string | undefined;

  constructor(
    private sideNavService: SideNavService,
    private authService: AuthService
  ){
    if (this.authService.userValue){
      this.roleName =  this.authService.userValue.role?.name;
    }
  }

  ngOnInit(){
    this.sideNavService.sideNaveToggleSubject.subscribe(()=>{
      this.sidenav.toggle();
    })
  } /* */
}

Update HomeComponent template to switch between menu depend on current user role:

<mtfs-header></mtfs-header>
<mat-sidenav-container class="container">
  <mat-sidenav #sidenav mode="side">    
    <mat-nav-list>
      @switch(this.roleName){
        @case('Admin') {<mtfs-navigation-admin></mtfs-navigation-admin>}
        @case('User') {<mtfs-navigation-user></mtfs-navigation-user>}
      }
    </mat-nav-list>
  </mat-sidenav>
  <mat-sidenav-content>   
    <div>
      <router-outlet></router-outlet>
    </div>
  </mat-sidenav-content>
</mat-sidenav-container> 

Create Users component for Admin role to test RBAC

Create libs/frontend/users folder:

Generate users-ui library:

npx nx g @nx/angular:library users-ui \\
--prefix mtfs \\
--importPath=@mtfs/frontend/users-ui \\
--tags type:ui,scope:frontend \\
--directory=libs/frontend/users/users-ui \\
--routing \\
--projectNameAndRootFormat=as-provided 

Generate Users Component:

npx nx g @nx/angular:component users \\
--prefix mtfs \\
--directory=libs/frontend/users/users-ui/src/lib/users \\
--style=scss \\
--nameAndDirectoryFormat=as-provided

add route for in admin navigation menu libs/frontend/ui/ui-components/src/lib/navigation-admin/navigation-admin.component.html:

<mat-nav-list>  
  <a mat-list-item  [routerLink]="['users']">Users</a>
  <a mat-list-item>Admin nav 1</a>
  <a mat-list-item>Admin nav 2</a>  
</mat-nav-list> 

Routing

To set up routing we will update apps/frontend/src/app/app.routes.ts file.

import { Route } from '@angular/router';
import { authGuard } from '@mtfs/frontend/auth-guards';
import { RoleEnum } from '@mtfs/shared/enums';

export const appRoutes: Route[] = [
  {
    path: 'auth',
     loadChildren: () =>  import('@mtfs/frontend/auth-ui').then((auth) => auth.authRoutes),

  }, 
    //----------path for admin role------------// 
  {
    path: '',
    async loadComponent() {
      const c = await import('@mtfs/frontend/ui-components');
      return c.HomeComponent
    },
    canActivate: [authGuard],   
    children: [
      {
        path: 'users',
        async loadComponent() {
          const c = await import('@mtfs/frontend/users-ui');
          return c.UsersComponent;
        },
        canActivate: [authGuard],
        data: {userRole: RoleEnum.Admin} 
      },
    ]  
  },
    //----------path for user role------------//
  {
    path: '',
    async loadComponent() {
      const c = await import('@mtfs/frontend/ui-components');
      return c.HomeComponent
    },
    canActivate: [authGuard], 

  },
  //----------Error Pages------------//
  {
    path: '403',
    async loadComponent(){
      const c = await import('@mtfs/frontend/error-pages');
      return c.ForbiddenComponent;      
    }      
  },

  {
    path: 'server-error',
    async loadComponent(){
      const c = await import('@mtfs/frontend/error-pages');
      return c.ServerErrorComponent
    }
  },
  {
    path: '**',
    async loadComponent(){
      const c = await import('@mtfs/frontend/error-pages');
      return c.NotFoundComponent
    }
  }
];

Now apps/frontend/src/app/app.routes.ts file include route for users component that allow access only for admin role:

    children: [
      {
        path: 'users',
        async loadComponent() {
          const c = await import('@mtfs/frontend/users-ui');
          return c.UsersComponent;
        },
        canActivate: [authGuard],
        data: {userRole: RoleEnum.Admin} 
      },
    ]

Summary and conclusions

So, we have developed the basic elements of a cookie-based authentication mechanism with support for role-based access. This mechanism includes:

  • The AuthService, which communicates with the server to perform user registration, user login, access token refresh, and logout;
  • The authGuard function checks whether a user is authenticated and authorized based on their roles. If not, it redirects them either to a login page or to a forbidden access page;
  • The authInterceptor handles token refresh logic for 401 Unauthorized errors, redirects on 403 Forbidden errors, and navigates to a server error page for other errors, while preserving credentials for requests;
  • Role Based navigation for User and Admin Role

Now we are ready to create a feature that will display a list of users from the database to test routing and the AuthInterceptor, and to finalize the error pages: not-found, forbidden, and server-error. This topic will be covered in the next article.


Discover more from More Than Fullstack

Subscribe to get the latest posts sent to your email.

Leave a Comment