In this section we will configure the Angular app to only allow users with the “Admin” role to access the admin feature in our app. It’s important to point out that there really is no such thing as “client side security”. All of our client side code (Angular) is downloaded by the client and all of it is accessible – the true security here is provided by the API. That said, we can do a pretty good job of hiding things from users who are not in that role, but this should not be used as the only means of securing the app.

First of all we need to resolve a bug! In a previous post we added the ability to edit a product, but we did not do anything with the images. This means that since when we get a product from the API, it has a URL, but we then send up the product to the API it already has the full URL which causes an issue as we can see here:

So in order to fix this we just need to add a line to the UpdateProduct method in the ProductsController so that we set the PictureUrl to whatever it was set to already:

        [HttpPut("{id}")]
        [Authorize(Roles = "Admin")]
        public async Task<ActionResult<Product>> UpdateProduct(int id, ProductCreateDto productToUpdate)
        {
            var product = await _unitOfWork.Repository<Product>().GetByIdAsync(id);

            productToUpdate.PictureUrl = product.PictureUrl;
            
            _mapper.Map(productToUpdate, product);

            _unitOfWork.Repository<Product>().Update(product);

            var result = await _unitOfWork.Complete();

            if (result <= 0) return BadRequest(new ApiResponse(400, "Problem updating product"));

            return Ok(product);
        }

This wil fix the bug. Now onto the Angular app to introduce the “security” we will be adding here.

First, lets add a new ReplaySubject observable so that we can store a boolean that tells us if a user is an admin in the account.service.ts:

export class AccountService {
  baseUrl = environment.apiUrl;
  private currentUserSource = new ReplaySubject<IUser>(1);
  currentUser$ = this.currentUserSource.asObservable();
  private isAdminSource = new ReplaySubject<boolean>(1);
  isAdmin$ = this.isAdminSource.asObservable();

Then lets add a method to check to see if the user is an admin or not. We can decode the jwt token using the atob method and since we know the JWT is split into 3 parts separated by a period, we can use the javascript split method to separate it into an array, then get the middle part, which contains the claims for the user:

  isAdmin(token: string): boolean {
    if (token) {
      const decodedToken = JSON.parse(atob(token.split('.')[1]));
      if (decodedToken.role.indexOf('Admin') > -1) {
        return true;
      }
    }
  }

With this in place we can then set the ReplaySubject when a user logs in or when we load the current user to which will check the user token and then set the isAdmin observable to true if they have this role:

  loadCurrentUser(token: string) {
    if (token === null) {
      this.currentUserSource.next(null);
      return of(null);
    }

    let headers = new HttpHeaders();
    headers = headers.set('Authorization', `Bearer ${token}`);

    return this.http.get(this.baseUrl + 'account', {headers}).pipe(
      map((user: IUser) => {
        if (user) {
          localStorage.setItem('token', user.token);
          this.currentUserSource.next(user);
          this.isAdminSource.next(this.isAdmin(user.token));
        }
      })
    );
  }
  login(values: any) {
    return this.http.post(this.baseUrl + 'account/login', values).pipe(
      map((user: IUser) => {
        if (user) {
          localStorage.setItem('token', user.token);
          this.currentUserSource.next(user);
          this.isAdminSource.next(this.isAdmin(user.token));
        }
      })
    );
  }

Now we have this we can create a new guard called admin.guard.ts that uses this to check if the user is and admin:

import {Injectable} from '@angular/core';
import {CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree, Router} from '@angular/router';
import {AccountService} from '../../account/account.service';
import {Observable} from 'rxjs';
import {map} from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class AdminGuard implements CanActivate {
  constructor(private accountService: AccountService, private router: Router) {
  }

  canActivate(
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): Observable<boolean> {
    return this.accountService.isAdmin$.pipe(
      map(admin => {
        if (admin) {
          return true;
        }
        this.router.navigateByUrl('/');
      })
    );
  }
}

Then we can add this to the app-routing.module.ts to protect the user from getting into the admin route:

  {
    path: 'admin',
    canActivate: [AuthGuard, AdminGuard],
    loadChildren: () => import('./admin/admin.module')
      .then(mod => mod.AdminModule), data: { breadcrumb: 'Admin' }
  },

Note that we can use multiple guards. These are evaluated in order and only if the all guards are returned as true does the user proceed. Now we just need to hide the Admin link in the nav bar if the user is not an admin and/or not logged in. We will make use of the async pipe again so the nav.component.ts will look like this:

export class NavBarComponent implements OnInit {
  basket$: Observable<IBasket>;
  currentUser$: Observable<IUser>;
  isAdmin$: Observable<boolean>;

  constructor(private basketService: BasketService, private accountService: AccountService) { }

  ngOnInit() {
    this.basket$ = this.basketService.basket$;
    this.currentUser$ = this.accountService.currentUser$;
    this.isAdmin$ = this.accountService.isAdmin$;
  }

Then in the nav.component.html we can just use an *ngIf statement to hide the link:

<a *ngIf="(isAdmin$ | async)" class="p-2" routerLink="/admin" routerLinkActive="active">Admin</a>

We now have the admin section of our client app “protected”, or hidden at least anyway. Progress.

The changes for this commit can be found here.

Next up, we will take a look at dealing with the product images.