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:
- Passing cookies with authentication credentials to the server with the request:
req = req.clone({
withCredentials: true,
});
- 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);
}
- 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.