Published: April 20 2023

Angular 15 Auth Boilerplate - Sign Up with Verification, Login and Forgot Password

Tutorial built with Angular 15.2.6

This is a detailed tutorial on how to implement a full-featured boilerplate sign up and authentication system in Angular 15.


Tutorial Contents


Angular Boilerplate App Overview

The auth boilerplate is one of the most comprehensive Angular examples I've posted so far, it includes the following features:

  • Email sign up and verification
  • JWT authentication with refresh tokens
  • Role based authorization with support for two roles (User & Admin)
  • Forgot password and reset password functionality
  • View and update my profile section
  • Admin section with sub section for managing all accounts (restricted to the Admin role)

Fake backend API

The app runs with a fake backend API by default to enable it to run completely in the browser without a real backend API. To disable the fake backend you just have to remove a couple of lines of code from the app module, you can build your own API or hook it up with the .NET API, Node.js + MongoDB API or Node.js + MySQL API available.

Fake emails displayed on screen by fake backend

There are no accounts registered in the application by default, in order to login you must first register and verify an account.

The fake backend displays "email" messages on screen for testing because it can't send real emails, so after registration a "verification email" is displayed with a link to verify the account just registered, click the link to verify the account and login to the Angular boilerplate app.

First account is an Admin

The first account registered is assigned to the Admin role and subsequent accounts are assigned to the regular User role. Admins can access the admin section and manage all accounts, regular user accounts can only update their own profile.

JWT authentication with refresh tokens

Authentication is implemented with JWT access tokens and refresh tokens. On successful authentication the api (or fake backend) returns a short lived JWT access token that expires after 15 minutes, and a refresh token that expires after 7 days in a cookie. The JWT is used for accessing secure routes on the api and the refresh token is used for generating new JWT access tokens when (or just before) they expire, the Angular app starts a timer to refresh the JWT token 1 minute before it expires to keep the account logged in.

Styled with Bootstrap 5

The example login app is styled with the CSS from Bootstrap 5.2, for more info about Bootstrap see https://getbootstrap.com/docs/5.2/getting-started/introduction/.

Code on GitHub

The example project is available on GitHub at https://github.com/cornflourblue/angular-15-signup-verification-boilerplate.

Here it is in action: (See on StackBlitz at https://stackblitz.com/edit/angular-15-signup-verification-boilerplate)


Run the Angular Auth Boilerplate Locally

  1. Install NodeJS and NPM from https://nodejs.org
  2. Download or clone the project source code from https://github.com/cornflourblue/angular-15-signup-verification-boilerplate
  3. Install all required npm packages by running npm install from the command line in the project root folder (where the package.json is located).
  4. Start the application by running npm start from the command line in the project root folder, this will launch a browser displaying the application.

NOTE: You can also start the app with the Angular CLI command ng serve --open. To do this first install the Angular CLI globally on your system with the command npm install -g @angular/cli.


Connect the Angular App with a .NET 6.0 API

For full details about the boilerplate .NET api see .NET 6.0 - Boilerplate API Tutorial with Email Sign Up, Verification, Authentication & Forgot Password. But to get up and running quickly just follow the below steps.

  1. Install the .NET SDK from https://dotnet.microsoft.com/download.
  2. Download or clone the tutorial project code from https://github.com/cornflourblue/dotnet-6-signup-verification-api
  3. Configure SMTP settings for email within the AppSettings section in the /appsettings.json file. For testing you can create a free account in one click at https://ethereal.email/ and copy the options below the title SMTP configuration.
  4. Start the api by running dotnet run from the command line in the project root folder (where the WebApi.csproj file is located), you should see the message Now listening on: http://localhost:4000.
  5. Back in the Angular app, remove or comment out the line below the comment // provider used to create fake backend located in the /src/app/app.module.ts file, then start the Angular app and it should now be hooked up with the .NET API.


Connect the Angular App with a Node.js + MongoDB API

For full details about the boilerplate Node + Mongo api see Node + Mongo - Boilerplate API with Email Sign Up, Verification, Authentication & Forgot Password. But to get up and running quickly just follow the below steps.

  1. Install MongoDB Community Server from  https://www.mongodb.com/download-center/community.
  2. Run MongoDB, instructions are available on the install page for each OS at https://docs.mongodb.com/manual/administration/install-community/
  3. Download or clone the project source code from https://github.com/cornflourblue/node-mongo-signup-verification-api
  4. Install all required npm packages by running npm install or npm i from the command line in the project root folder (where the package.json is located).
  5. Configure SMTP settings for email within the smtpOptions property in the /src/config.json file. For testing you can create a free account in one click at https://ethereal.email/ and copy the options below the title Nodemailer configuration.
  6. Start the api by running npm start from the command line in the project root folder, you should see the message Server listening on port 4000.
  7. Back in the Angular app, remove or comment out the line below the comment // provider used to create fake backend located in the /src/app/app.module.ts file, then start the Angular app and it should now be hooked up with the Node + Mongo API.


Connect the Angular App with a Node.js + MySQL API

For full details about the boilerplate Node.js + MySQL api see Node.js + MySQL - Boilerplate API with Email Sign Up, Verification, Authentication & Forgot Password. But to get up and running quickly just follow the below steps.

  1. Install MySQL Community Server from https://dev.mysql.com/downloads/mysql/ and ensure it is started. Installation instructions are available at https://dev.mysql.com/doc/refman/8.0/en/installing.html.
  2. Download or clone the project source code from https://github.com/cornflourblue/node-mysql-signup-verification-api
  3. Install all required npm packages by running npm install or npm i from the command line in the project root folder (where the package.json is located).
  4. Configure SMTP settings for email within the smtpOptions property in the /src/config.json file. For testing you can create a free account in one click at https://ethereal.email/ and copy the options below the title Nodemailer configuration.
  5. Start the api by running npm start from the command line in the project root folder, you should see the message Server listening on port 4000.
  6. Back in the Angular app, remove or comment out the line below the comment // provider used to create fake backend located in the /src/app/app.module.ts file, then start the Angular app and it should now be hooked up with the Node + MySQL API.


Angular 15 Boilerplate Code Documentation

The Angular CLI was used to generate the base project structure with the ng new <project name> command, the CLI is also used to build and serve the boilerplate application. For more info about the Angular CLI see https://angular.io/cli.

The app and code structure of the tutorial mostly follow the best practice recommendations in the official Angular Style Guide, with a few of my own tweaks here and there.

Folder Structure

Each feature has it's own folder (account, admin, home & profile), other shared/common code such as components, services, models, helpers etc are placed in folders prefixed with an underscore _ to easily differentiate them from features and group them together at the top of the folder structure.

Lazy Loaded Feature Modules

The account, admin and profile features are organised into self contained feature modules that manage their own layout, routes and components, and are hooked into the main app inside the app routing module with lazy loading.

Barrel Files

The index.ts files in some folders are barrel files that group the exported modules from that folder together so they can be imported using only the folder path instead of the full module path, and to enable importing multiple modules in a single import (e.g. import { AccountService, AlertService } from '@app/_services').

TypeScript Path Aliases

Path aliases @app and @environments have been configured in tsconfig.json that map to the /src/app and /src/environments directories. This allows imports to be relative to the app and environments folders by prefixing import paths with aliases instead of having to use long relative paths (e.g. import MyComponent from '../../../MyComponent').

The Angular Code

Here are the main project files that contain the boilerplate application logic, I left out some files that were generated by Angular CLI ng new command that I didn't change.

 

Alert Component Template

Path: /src/app/_components/alert.component.html

The alert component template contains the html for displaying alert messages at the top of the page, it renders a notification for each alert in the alerts array of the alert component below.

<div *ngIf="alerts.length" class="container">
    <div class="m-3">
        <div *ngFor="let alert of alerts" class="{{cssClasses(alert)}}">
            <span [innerHTML]="alert.message"></span>
            <button class="btn-close" (click)="removeAlert(alert)"></button>
        </div>
    </div>
</div>
 

Alert Component

Path: /src/app/_components/alert.component.ts

The alert component controls the adding & removing of alerts in the UI, it maintains an array of alerts that are rendered by the component template.

The component subscribes to receive new alerts from the alert service in the ngOnInit method by calling the alertService.onAlert() method, new alerts are added to the alerts array for display. Alerts are cleared when an alert with an empty message is received from the alert service. The ngOnInit method also calls router.events.subscribe() to subscribe to route change events so it can automatically clear alerts on route changes.

The ngOnDestroy() method unsubscribes from the alert service and router events when the component is destroyed to prevent memory leaks from orphaned subscriptions.

The removeAlert() method removes the specified alert object from the array, which allows individual alerts to be closed in the UI.

The cssClasses() method returns corresponding bootstrap alert classes for each of the alert types, if you're using something other than bootstrap you can change the CSS classes returned to suit your application.

For more info about Angular alerts see Angular 14 - Alert (Toaster) Notifications Tutorial & Example.

import { Component, OnInit, OnDestroy, Input } from '@angular/core';
import { Router, NavigationStart } from '@angular/router';
import { Subscription } from 'rxjs';

import { Alert, AlertType } from '@app/_models';
import { AlertService } from '@app/_services';

@Component({ selector: 'alert', templateUrl: 'alert.component.html' })
export class AlertComponent implements OnInit, OnDestroy {
    @Input() id = 'default-alert';
    @Input() fade = true;

    alerts: Alert[] = [];
    alertSubscription!: Subscription;
    routeSubscription!: Subscription;

    constructor(private router: Router, private alertService: AlertService) { }

    ngOnInit() {
        // subscribe to new alert notifications
        this.alertSubscription = this.alertService.onAlert(this.id)
            .subscribe(alert => {
                // clear alerts when an empty alert is received
                if (!alert.message) {
                    // filter out alerts without 'keepAfterRouteChange' flag
                    this.alerts = this.alerts.filter(x => x.keepAfterRouteChange);

                    // remove 'keepAfterRouteChange' flag on the rest
                    this.alerts.forEach(x => delete x.keepAfterRouteChange);
                    return;
                }

                // add alert to array
                this.alerts.push(alert);

                // auto close alert if required
                if (alert.autoClose) {
                    setTimeout(() => this.removeAlert(alert), 3000);
                }
            });

        // clear alerts on location change
        this.routeSubscription = this.router.events.subscribe(event => {
            if (event instanceof NavigationStart) {
                this.alertService.clear(this.id);
            }
        });
    }

    ngOnDestroy() {
        // unsubscribe to avoid memory leaks
        this.alertSubscription.unsubscribe();
        this.routeSubscription.unsubscribe();
    }

    removeAlert(alert: Alert) {
        // check if already removed to prevent error on auto close
        if (!this.alerts.includes(alert)) return;

        if (this.fade) {
            // fade out alert
            alert.fade = true;

            // remove alert after faded out
            setTimeout(() => {
                this.alerts = this.alerts.filter(x => x !== alert);
            }, 250);
        } else {
            // remove alert
            this.alerts = this.alerts.filter(x => x !== alert);
        }
    }

    cssClasses(alert: Alert) {
        if (!alert) return;

        const classes = ['alert', 'alert-dismissible', 'mt-4', 'container'];

        const alertTypeClass = {
            [AlertType.Success]: 'alert-success',
            [AlertType.Error]: 'alert-danger',
            [AlertType.Info]: 'alert-info',
            [AlertType.Warning]: 'alert-warning'
        }

        if (alert.type !== undefined) {
            classes.push(alertTypeClass[alert.type]);
        }

        if (alert.fade) {
            classes.push('fade');
        }

        return classes.join(' ');
    }
}
 

App Initializer

Path: /src/app/_helpers/app.initializer.ts

The app initializer runs before the boilerplate app starts up and attempts to automatically authenticate the user in the background by calling accountService.refreshToken() to get a new JWT token from the api using the refresh token stored in a browser cookie (if it exists).

If the user has logged in previously (without logging out) and the browser still contains a valid refresh token cookie then they will be automatically logged in when the app loads.

The catchError() operator is used to ensure the observable always reaches the completed state even if the refreshToken() request fails.

The app initializer is added to angular app in the providers section of the app module using the APP_INITIALIZER injection token. For more info see Angular - Execute an init function before app startup with an Angular APP_INITIALIZER.

import { catchError, of } from 'rxjs';

import { AccountService } from '@app/_services';

export function appInitializer(accountService: AccountService) {
    return () => accountService.refreshToken()
        .pipe(
            // catch error to start app on success or failure
            catchError(() => of())
        );
}
 

Auth Guard

Path: /src/app/_helpers/auth.guard.ts

The auth guard is an angular route guard that's used to prevent unauthorized users from accessing restricted routes, it does this by implementing the CanActivate interface which allows the guard to decide if a route can be activated with the canActivate() method. If the method returns true the route is activated (allowed to proceed), otherwise if the method returns false the route is blocked.

The auth guard uses the account service to check if the user is logged in, if the user isn't logged in they're redirected to the /login page with the returnUrl in the query parameters, if the user is logged in but not in an authorized role they're redirected to the home page

Angular route guards are attached to routes in the router config, this auth guard is used in app-routing.module.ts to protect the home, profile and admin routes.

import { Injectable } from '@angular/core';
import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';

import { AccountService } from '@app/_services';

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

    canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
        const account = this.accountService.accountValue;
        if (account) {
            // check if route is restricted by role
            if (route.data.roles && !route.data.roles.includes(account.role)) {
                // role not authorized so redirect to home page
                this.router.navigate(['/']);
                return false;
            }

            // authorized so return true
            return true;
        }

        // not logged in so redirect to login page with the return url 
        this.router.navigate(['/account/login'], { queryParams: { returnUrl: state.url } });
        return false;
    }
}
 

Error Interceptor

Path: /src/app/_helpers/error.interceptor.ts

The Error Interceptor intercepts http responses from the api to check if there were any errors. If there is a 401 Unauthorized or 403 Forbidden response the account is automatically logged out of the boilerplate application, all other errors are re-thrown up to the calling service so an alert with the error can be displayed on the screen.

It's implemented using the Angular HttpInterceptor interface included in the HttpClientModule, by implementing the HttpInterceptor interface you can create a custom interceptor to catch all error responses from the api in a single location.

Http interceptors are added to the request pipeline in the providers section of the app.module.ts file.

import { Injectable } from '@angular/core';
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';

import { AccountService } from '@app/_services';

@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
    constructor(private accountService: AccountService) { }

    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        return next.handle(request).pipe(catchError(err => {
            if ([401, 403].includes(err.status) && this.accountService.accountValue) {
                // auto logout if 401 or 403 response returned from api
                this.accountService.logout();
            }

            const error = (err && err.error && err.error.message) || err.statusText;
            console.error(err);
            return throwError(() => error);
        }))
    }
}
 

Fake Backend API

Path: /src/app/_helpers/fake-backend.ts

In order to run and test the Angular boilerplate app without a real backend API, the example uses a fake backend that intercepts the HTTP requests from the Angular app and sends back "fake" responses. This is done by a class that implements the Angular HttpInterceptor interface, for more information on Angular HTTP Interceptors see https://angular.io/api/common/http/HttpInterceptor.

The fake backend is organised into a top level handleRoute() function that checks the request url and method to determine how the request should be handled. For intercepted routes one of the below // route functions is called, for all other routes the request is passed through to the real backend by calling next.handle(request). Below the route functions there are // helper functions for returning different response types and performing small tasks.

The fake backend can't send emails so instead displays "email" messages on the screen by calling alertService.info() with the contents of the email e.g. "verify email" and "reset password" emails.

For more info see Angular 14 - Fake Backend API to Intercept HTTP Requests in Development.

import { Injectable } from '@angular/core';
import { HttpRequest, HttpResponse, HttpHandler, HttpEvent, HttpInterceptor, HTTP_INTERCEPTORS } from '@angular/common/http';
import { Observable, of, throwError } from 'rxjs';
import { delay, materialize, dematerialize } from 'rxjs/operators';

import { AlertService } from '@app/_services';
import { Role } from '@app/_models';

// array in local storage for accounts
const accountsKey = 'angular-15-signup-verification-boilerplate-accounts';
let accounts: any[] = JSON.parse(localStorage.getItem(accountsKey)!) || [];

@Injectable()
export class FakeBackendInterceptor implements HttpInterceptor {
    constructor(private alertService: AlertService) { }

    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        const { url, method, headers, body } = request;
        const alertService = this.alertService;

        return handleRoute();

        function handleRoute() {
            switch (true) {
                case url.endsWith('/accounts/authenticate') && method === 'POST':
                    return authenticate();
                case url.endsWith('/accounts/refresh-token') && method === 'POST':
                    return refreshToken();
                case url.endsWith('/accounts/revoke-token') && method === 'POST':
                    return revokeToken();
                case url.endsWith('/accounts/register') && method === 'POST':
                    return register();
                case url.endsWith('/accounts/verify-email') && method === 'POST':
                    return verifyEmail();
                case url.endsWith('/accounts/forgot-password') && method === 'POST':
                    return forgotPassword();
                case url.endsWith('/accounts/validate-reset-token') && method === 'POST':
                    return validateResetToken();
                case url.endsWith('/accounts/reset-password') && method === 'POST':
                    return resetPassword();
                case url.endsWith('/accounts') && method === 'GET':
                    return getAccounts();
                case url.match(/\/accounts\/\d+$/) && method === 'GET':
                    return getAccountById();
                case url.endsWith('/accounts') && method === 'POST':
                    return createAccount();
                case url.match(/\/accounts\/\d+$/) && method === 'PUT':
                    return updateAccount();
                case url.match(/\/accounts\/\d+$/) && method === 'DELETE':
                    return deleteAccount();
                default:
                    // pass through any requests not handled above
                    return next.handle(request);
            }    
        }

        // route functions

        function authenticate() {
            const { email, password } = body;
            const account = accounts.find(x => x.email === email && x.password === password && x.isVerified);
            
            if (!account) return error('Email or password is incorrect');

            // add refresh token to account
            account.refreshTokens.push(generateRefreshToken());
            localStorage.setItem(accountsKey, JSON.stringify(accounts));

            return ok({
                ...basicDetails(account),
                jwtToken: generateJwtToken(account)
            });
        }

        function refreshToken() {
            const refreshToken = getRefreshToken();
            
            if (!refreshToken) return unauthorized();

            const account = accounts.find(x => x.refreshTokens.includes(refreshToken));
            
            if (!account) return unauthorized();

            // replace old refresh token with a new one and save
            account.refreshTokens = account.refreshTokens.filter((x: any) => x !== refreshToken);
            account.refreshTokens.push(generateRefreshToken());
            localStorage.setItem(accountsKey, JSON.stringify(accounts));

            return ok({
                ...basicDetails(account),
                jwtToken: generateJwtToken(account)
            });
        }

        function revokeToken() {
            if (!isAuthenticated()) return unauthorized();
            
            const refreshToken = getRefreshToken();
            const account = accounts.find(x => x.refreshTokens.includes(refreshToken));
            
            // revoke token and save
            account.refreshTokens = account.refreshTokens.filter((x: any) => x !== refreshToken);
            localStorage.setItem(accountsKey, JSON.stringify(accounts));

            return ok();
        }

        function register() {
            const account = body;

            if (accounts.find(x => x.email === account.email)) {
                // display email already registered "email" in alert
                setTimeout(() => {
                    alertService.info(`
                        <h4>Email Already Registered</h4>
                        <p>Your email ${account.email} is already registered.</p>
                        <p>If you don't know your password please visit the <a href="${location.origin}/account/forgot-password">forgot password</a> page.</p>
                        <div><strong>NOTE:</strong> The fake backend displayed this "email" so you can test without an api. A real backend would send a real email.</div>
                    `, { autoClose: false });
                }, 1000);

                // always return ok() response to prevent email enumeration
                return ok();
            }

            // assign account id and a few other properties then save
            account.id = newAccountId();
            if (account.id === 1) {
                // first registered account is an admin
                account.role = Role.Admin;
            } else {
                account.role = Role.User;
            }
            account.dateCreated = new Date().toISOString();
            account.verificationToken = new Date().getTime().toString();
            account.isVerified = false;
            account.refreshTokens = [];
            delete account.confirmPassword;
            accounts.push(account);
            localStorage.setItem(accountsKey, JSON.stringify(accounts));

            // display verification email in alert
            setTimeout(() => {
                const verifyUrl = `${location.origin}/account/verify-email?token=${account.verificationToken}`;
                alertService.info(`
                    <h4>Verification Email</h4>
                    <p>Thanks for registering!</p>
                    <p>Please click the below link to verify your email address:</p>
                    <p><a href="${verifyUrl}">${verifyUrl}</a></p>
                    <div><strong>NOTE:</strong> The fake backend displayed this "email" so you can test without an api. A real backend would send a real email.</div>
                `, { autoClose: false });
            }, 1000);

            return ok();
        }
        
        function verifyEmail() {
            const { token } = body;
            const account = accounts.find(x => !!x.verificationToken && x.verificationToken === token);
            
            if (!account) return error('Verification failed');
            
            // set is verified flag to true if token is valid
            account.isVerified = true;
            localStorage.setItem(accountsKey, JSON.stringify(accounts));

            return ok();
        }

        function forgotPassword() {
            const { email } = body;
            const account = accounts.find(x => x.email === email);
            
            // always return ok() response to prevent email enumeration
            if (!account) return ok();
            
            // create reset token that expires after 24 hours
            account.resetToken = new Date().getTime().toString();
            account.resetTokenExpires = new Date(Date.now() + 24*60*60*1000).toISOString();
            localStorage.setItem(accountsKey, JSON.stringify(accounts));

            // display password reset email in alert
            setTimeout(() => {
                const resetUrl = `${location.origin}/account/reset-password?token=${account.resetToken}`;
                alertService.info(`
                    <h4>Reset Password Email</h4>
                    <p>Please click the below link to reset your password, the link will be valid for 1 day:</p>
                    <p><a href="${resetUrl}">${resetUrl}</a></p>
                    <div><strong>NOTE:</strong> The fake backend displayed this "email" so you can test without an api. A real backend would send a real email.</div>
                `, { autoClose: false });
            }, 1000);

            return ok();
        }
        
        function validateResetToken() {
            const { token } = body;
            const account = accounts.find(x =>
                !!x.resetToken && x.resetToken === token &&
                new Date() < new Date(x.resetTokenExpires)
            );
            
            if (!account) return error('Invalid token');
            
            return ok();
        }

        function resetPassword() {
            const { token, password } = body;
            const account = accounts.find(x =>
                !!x.resetToken && x.resetToken === token &&
                new Date() < new Date(x.resetTokenExpires)
            );
            
            if (!account) return error('Invalid token');
            
            // update password and remove reset token
            account.password = password;
            account.isVerified = true;
            delete account.resetToken;
            delete account.resetTokenExpires;
            localStorage.setItem(accountsKey, JSON.stringify(accounts));

            return ok();
        }

        function getAccounts() {
            if (!isAuthenticated()) return unauthorized();
            return ok(accounts.map(x => basicDetails(x)));
        }

        function getAccountById() {
            if (!isAuthenticated()) return unauthorized();

            let account = accounts.find(x => x.id === idFromUrl());

            // user accounts can get own profile and admin accounts can get all profiles
            if (account.id !== currentAccount().id && !isAuthorized(Role.Admin)) {
                return unauthorized();
            }

            return ok(basicDetails(account));
        }

        function createAccount() {
            if (!isAuthorized(Role.Admin)) return unauthorized();

            const account = body;
            if (accounts.find(x => x.email === account.email)) {
                return error(`Email ${account.email} is already registered`);
            }

            // assign account id and a few other properties then save
            account.id = newAccountId();
            account.dateCreated = new Date().toISOString();
            account.isVerified = true;
            account.refreshTokens = [];
            delete account.confirmPassword;
            accounts.push(account);
            localStorage.setItem(accountsKey, JSON.stringify(accounts));

            return ok();
        }

        function updateAccount() {
            if (!isAuthenticated()) return unauthorized();

            let params = body;
            let account = accounts.find(x => x.id === idFromUrl());

            // user accounts can update own profile and admin accounts can update all profiles
            if (account.id !== currentAccount().id && !isAuthorized(Role.Admin)) {
                return unauthorized();
            }

            // only update password if included
            if (!params.password) {
                delete params.password;
            }
            // don't save confirm password
            delete params.confirmPassword;

            // update and save account
            Object.assign(account, params);
            localStorage.setItem(accountsKey, JSON.stringify(accounts));

            return ok(basicDetails(account));
        }

        function deleteAccount() {
            if (!isAuthenticated()) return unauthorized();

            let account = accounts.find(x => x.id === idFromUrl());

            // user accounts can delete own account and admin accounts can delete any account
            if (account.id !== currentAccount().id && !isAuthorized(Role.Admin)) {
                return unauthorized();
            }

            // delete account then save
            accounts = accounts.filter(x => x.id !== idFromUrl());
            localStorage.setItem(accountsKey, JSON.stringify(accounts));
            return ok();
        }
        
        // helper functions

        function ok(body?: any) {
            return of(new HttpResponse({ status: 200, body }))
                .pipe(delay(500)); // delay observable to simulate server api call
        }

        function error(message: string) {
            return throwError(() => ({ error: { message } }))
                .pipe(materialize(), delay(500), dematerialize()); // call materialize and dematerialize to ensure delay even if an error is thrown (https://github.com/Reactive-Extensions/RxJS/issues/648);
        }

        function unauthorized() {
            return throwError(() => ({ status: 401, error: { message: 'Unauthorized' } }))
                .pipe(materialize(), delay(500), dematerialize());
        }

        function basicDetails(account: any) {
            const { id, title, firstName, lastName, email, role, dateCreated, isVerified } = account;
            return { id, title, firstName, lastName, email, role, dateCreated, isVerified };
        }

        function isAuthenticated() {
            return !!currentAccount();
        }

        function isAuthorized(role: any) {
            const account = currentAccount();
            if (!account) return false;
            return account.role === role;
        }

        function idFromUrl() {
            const urlParts = url.split('/');
            return parseInt(urlParts[urlParts.length - 1]);
        }

        function newAccountId() {
            return accounts.length ? Math.max(...accounts.map(x => x.id)) + 1 : 1;
        }

        function currentAccount() {
            // check if jwt token is in auth header
            const authHeader = headers.get('Authorization');
            if (!authHeader?.startsWith('Bearer fake-jwt-token')) return;

            // check if token is expired
            const jwtToken = JSON.parse(atob(authHeader.split('.')[1]));
            const tokenExpired = Date.now() > (jwtToken.exp * 1000);
            if (tokenExpired) return;

            const account = accounts.find(x => x.id === jwtToken.id);
            return account;
        }

        function generateJwtToken(account: any) {
            // create token that expires in 15 minutes
            const tokenPayload = { 
                exp: Math.round(new Date(Date.now() + 15*60*1000).getTime() / 1000),
                id: account.id
            }
            return `fake-jwt-token.${btoa(JSON.stringify(tokenPayload))}`;
        }

        function generateRefreshToken() {
            const token = new Date().getTime().toString();

            // add token cookie that expires in 7 days
            const expires = new Date(Date.now() + 7*24*60*60*1000).toUTCString();
            document.cookie = `fakeRefreshToken=${token}; expires=${expires}; path=/`;

            return token;
        }

        function getRefreshToken() {
            // get refresh token from cookie
            return (document.cookie.split(';').find(x => x.includes('fakeRefreshToken')) || '=').split('=')[1];
        }
    }
}

export let fakeBackendProvider = {
    // use fake backend in place of Http service for backend-less development
    provide: HTTP_INTERCEPTORS,
    useClass: FakeBackendInterceptor,
    multi: true
};
 

JWT Interceptor

Path: /src/app/_helpers/jwt.interceptor.ts

The JWT Interceptor intercepts http requests from the application to add a JWT auth token to the Authorization header if the user is logged in and the request is to the application api url (environment.apiUrl).

It's implemented using the HttpInterceptor interface included in the HttpClientModule, by implementing the HttpInterceptor interface you can create a custom interceptor to modify http requests before they get sent to the server.

Http interceptors are added to the request pipeline in the providers section of the app.module.ts file.

import { Injectable } from '@angular/core';
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http';
import { Observable } from 'rxjs';

import { environment } from '@environments/environment';
import { AccountService } from '@app/_services';

@Injectable()
export class JwtInterceptor implements HttpInterceptor {
    constructor(private accountService: AccountService) { }

    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        // add auth header with jwt if account is logged in and request is to the api url
        const account = this.accountService.accountValue;
        const isLoggedIn = account && account.jwtToken;
        const isApiUrl = request.url.startsWith(environment.apiUrl);
        if (isLoggedIn && isApiUrl) {
            request = request.clone({
                setHeaders: { Authorization: `Bearer ${account.jwtToken}` }
            });
        }

        return next.handle(request);
    }
}
 

Must Match Validator

Path: /src/app/_helpers/must-match.validator.ts

The reactive forms custom MustMatch validator is used by some components in the boilerplate app to validate that password and confirmPassword fields match, e.g. in the register component.

For more info on cross field validation see Angular - Multiple Field (Cross Field) Validation with Reactive Forms.

import { AbstractControl } from '@angular/forms';

// custom validator to check that two fields match
export function MustMatch(controlName: string, matchingControlName: string) {
    return (group: AbstractControl) => {
        const control = group.get(controlName);
        const matchingControl = group.get(matchingControlName);

        if (!control || !matchingControl) {
            return null;
        }

        // return if another validator has already found an error on the matchingControl
        if (matchingControl.errors && !matchingControl.errors.mustMatch) {
            return null;
        }

        // set error on matchingControl if validation fails
        if (control.value !== matchingControl.value) {
            matchingControl.setErrors({ mustMatch: true });
        } else {
            matchingControl.setErrors(null);
        }
        return null;
    }
}
 

Account Model

Path: /src/app/_models/account.ts

The account model is a small class that defines the properties of an account.

import { Role } from './role';

export class Account {
    id?: string;
    title?: string;
    firstName?: string;
    lastName?: string;
    email?: string;
    role?: Role;
    jwtToken?: string;
}
 

Alert Models

Path: /src/app/_models/alert.ts

The alert model file contains the Alert, AlertType and AlertOptions models.

  • Alert defines the properties of each alert object.
  • AlertType is an enumeration containing the types of alerts.
  • AlertOptions defines the options available when sending an alert to the alert service.
export class Alert {
    id?: string;
    type?: AlertType;
    message?: string;
    autoClose?: boolean;
    keepAfterRouteChange?: boolean;
    fade?: boolean;

    constructor(init?: Partial<Alert>) {
        Object.assign(this, init);
    }
}

export enum AlertType {
    Success,
    Error,
    Info,
    Warning
}

export class AlertOptions {
    id?: string;
    autoClose?: boolean;
    keepAfterRouteChange?: boolean;
}
 

Role Enum

Path: /src/app/_models/role.ts

The role enum defines the roles that are supported by the application.

export enum Role {
    User = 'User',
    Admin = 'Admin'
}
 

Account Service

Path: /src/app/_services/account.service.ts

The account service handles communication between the Angular auth boilerplate and the backend api for everything related to accounts. It contains methods for the sign up, verification, authentication, refresh token, forgot password and reset password, as well as standard CRUD methods for retrieving and modifying account data.

On successful login the api returns the account details and a JWT token which are published to all subscribers with the call to this.accountSubject.next(account), the api also returns a refresh token cookie which is stored by the browser. The method then starts a countdown timer by calling this.startRefreshTokenTimer() to auto refresh the JWT token in the background (silent refresh) one minute before it expires in order to keep the account logged in.

The logout() method makes a POST request to the API to revoke the refresh token that is stored in the refreshToken cookie in the browser, cancels the silent refresh running in the background by calling this.stopRefreshTokenTimer(), then logs the user out by publishing a null value to all subscriber components (this.accountSubject.next(null)), and lastly redirects to the login page.

The account property exposes an RxJS observable (Observable<Account>) so any component can subscribe to be notified when a user logs in, logs out, has their token refreshed or updates their profile. The notification is triggered by the call to this.accountSubject.next() from each of the corresponding methods in the service. For more info on component communication with RxJS see Angular 14 - Communicating Between Components with RxJS Observable & Subject.

import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, Observable } from 'rxjs';
import { map, finalize } from 'rxjs/operators';

import { environment } from '@environments/environment';
import { Account } from '@app/_models';

const baseUrl = `${environment.apiUrl}/accounts`;

@Injectable({ providedIn: 'root' })
export class AccountService {
    private accountSubject: BehaviorSubject<Account | null>;
    public account: Observable<Account | null>;

    constructor(
        private router: Router,
        private http: HttpClient
    ) {
        this.accountSubject = new BehaviorSubject<Account | null>(null);
        this.account = this.accountSubject.asObservable();
    }

    public get accountValue() {
        return this.accountSubject.value;
    }

    login(email: string, password: string) {
        return this.http.post<any>(`${baseUrl}/authenticate`, { email, password }, { withCredentials: true })
            .pipe(map(account => {
                this.accountSubject.next(account);
                this.startRefreshTokenTimer();
                return account;
            }));
    }

    logout() {
        this.http.post<any>(`${baseUrl}/revoke-token`, {}, { withCredentials: true }).subscribe();
        this.stopRefreshTokenTimer();
        this.accountSubject.next(null);
        this.router.navigate(['/account/login']);
    }

    refreshToken() {
        return this.http.post<any>(`${baseUrl}/refresh-token`, {}, { withCredentials: true })
            .pipe(map((account) => {
                this.accountSubject.next(account);
                this.startRefreshTokenTimer();
                return account;
            }));
    }

    register(account: Account) {
        return this.http.post(`${baseUrl}/register`, account);
    }

    verifyEmail(token: string) {
        return this.http.post(`${baseUrl}/verify-email`, { token });
    }

    forgotPassword(email: string) {
        return this.http.post(`${baseUrl}/forgot-password`, { email });
    }

    validateResetToken(token: string) {
        return this.http.post(`${baseUrl}/validate-reset-token`, { token });
    }

    resetPassword(token: string, password: string, confirmPassword: string) {
        return this.http.post(`${baseUrl}/reset-password`, { token, password, confirmPassword });
    }

    getAll() {
        return this.http.get<Account[]>(baseUrl);
    }

    getById(id: string) {
        return this.http.get<Account>(`${baseUrl}/${id}`);
    }

    create(params: any) {
        return this.http.post(baseUrl, params);
    }

    update(id: string, params: any) {
        return this.http.put(`${baseUrl}/${id}`, params)
            .pipe(map((account: any) => {
                // update the current account if it was updated
                if (account.id === this.accountValue?.id) {
                    // publish updated account to subscribers
                    account = { ...this.accountValue, ...account };
                    this.accountSubject.next(account);
                }
                return account;
            }));
    }

    delete(id: string) {
        return this.http.delete(`${baseUrl}/${id}`)
            .pipe(finalize(() => {
                // auto logout if the logged in account was deleted
                if (id === this.accountValue?.id)
                    this.logout();
            }));
    }

    // helper methods

    private refreshTokenTimeout?: any;

    private startRefreshTokenTimer() {
        // parse json object from base64 encoded jwt token
        const jwtBase64 = this.accountValue!.jwtToken!.split('.')[1];
        const jwtToken = JSON.parse(atob(jwtBase64));

        // set a timeout to refresh the token a minute before it expires
        const expires = new Date(jwtToken.exp * 1000);
        const timeout = expires.getTime() - Date.now() - (60 * 1000);
        this.refreshTokenTimeout = setTimeout(() => this.refreshToken().subscribe(), timeout);
    }

    private stopRefreshTokenTimer() {
        clearTimeout(this.refreshTokenTimeout);
    }
}
 

Alert Service

Path: /src/app/_services/alert.service.ts

The alert service acts as the bridge between any Angular component and the alert component that renders the alert messages. It contains methods for sending, clearing and subscribing to alert messages.

You can trigger alert notifications from any component or service by calling one of the convenience methods for displaying the different types of alerts: success(), error(), info() and warn().

Alert method parameters

  • The first parameter is a string for the alert message which can be a plain text or HTML.
  • The second parameter is an optional AlertOptions object that supports the following properties (all are optional):
    • id - the id of the <alert> component that will display the alert notification. Default value is "default-alert".
    • autoClose - if true the alert will automatically close the after three seconds. Default value is true.
    • keepAfterRouteChange - if true the alert will continue to display after one route change, which is useful to display an alert after an automatic redirect (e.g. after completing a form). Default value is false.

The alert service uses the RxJS Observable and Subject classes to enable communication with other components, for more information on how this works see Angular 14 - Communicating Between Components with RxJS Observable & Subject.

import { Injectable } from '@angular/core';
import { Observable, Subject } from 'rxjs';
import { filter } from 'rxjs/operators';

import { Alert, AlertOptions, AlertType } from '@app/_models';

@Injectable({ providedIn: 'root' })
export class AlertService {
    private subject = new Subject<Alert>();
    private defaultId = 'default-alert';

    // enable subscribing to alerts observable
    onAlert(id = this.defaultId): Observable<Alert> {
        return this.subject.asObservable().pipe(filter(x => x && x.id === id));
    }

    // convenience methods
    success(message: string, options?: AlertOptions) {
        this.alert(new Alert({ ...options, type: AlertType.Success, message }));
    }

    error(message: string, options?: AlertOptions) {
        this.alert(new Alert({ ...options, type: AlertType.Error, message }));
    }

    info(message: string, options?: AlertOptions) {
        this.alert(new Alert({ ...options, type: AlertType.Info, message }));
    }

    warn(message: string, options?: AlertOptions) {
        this.alert(new Alert({ ...options, type: AlertType.Warning, message }));
    }

    // core alert method
    alert(alert: Alert) {
        alert.id = alert.id || this.defaultId;
        alert.autoClose = (alert.autoClose === undefined ? true : alert.autoClose);
        this.subject.next(alert);
    }

    // clear alerts
    clear(id = this.defaultId) {
        this.subject.next(new Alert({ id }));
    }
}
 

Account Routing Module

Path: /src/app/account/account-routing.module.ts

The account routing module defines the routes for the account feature module. It includes routes for login, registration and related functionality, and a parent route for the account layout component which contains the common layout code for the account section.

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { LayoutComponent } from './layout.component';
import { LoginComponent } from './login.component';
import { RegisterComponent } from './register.component';
import { VerifyEmailComponent } from './verify-email.component';
import { ForgotPasswordComponent } from './forgot-password.component';
import { ResetPasswordComponent } from './reset-password.component';

const routes: Routes = [
    {
        path: '', component: LayoutComponent,
        children: [
            { path: 'login', component: LoginComponent },
            { path: 'register', component: RegisterComponent },
            { path: 'verify-email', component: VerifyEmailComponent },
            { path: 'forgot-password', component: ForgotPasswordComponent },
            { path: 'reset-password', component: ResetPasswordComponent }
        ]
    }
];

@NgModule({
    imports: [RouterModule.forChild(routes)],
    exports: [RouterModule]
})
export class AccountRoutingModule { }
 

Account Feature Module

Path: /src/app/account/account.module.ts

The account module defines the feature module for the account section along with metadata about the module. The imports specify which other angular modules are required by this module, and the declarations specify which components belong to this module. For more info on angular feature modules see https://angular.io/guide/feature-modules.

The account module is hooked into the main app inside the app routing module with lazy loading.

import { NgModule } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';

import { AccountRoutingModule } from './account-routing.module';
import { LayoutComponent } from './layout.component';
import { LoginComponent } from './login.component';
import { RegisterComponent } from './register.component';
import { VerifyEmailComponent } from './verify-email.component';
import { ForgotPasswordComponent } from './forgot-password.component';
import { ResetPasswordComponent } from './reset-password.component';

@NgModule({
    imports: [
        CommonModule,
        ReactiveFormsModule,
        AccountRoutingModule
    ],
    declarations: [
        LayoutComponent,
        LoginComponent,
        RegisterComponent,
        VerifyEmailComponent,
        ForgotPasswordComponent,
        ResetPasswordComponent
    ]
})
export class AccountModule { }
 

Forgot Password Component Template

Path: /src/app/account/forgot-password.component.html

The forgot password component template contains a simple form with a single field for entering the email of the account that you have forgotten password for. The form element uses the [formGroup] directive to bind to the form FormGroup in the forgot password component below, and it binds the form submit event to the onSubmit() handler in the forgot password component using the angular event binding (ngSubmit)="onSubmit()".

<h3 class="card-header">Forgot Password</h3>
<div class="card-body">
    <form [formGroup]="form" (ngSubmit)="onSubmit()">
        <div class="mb-3">
            <label class="form-label">Email</label>
            <input type="text" formControlName="email" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.email.errors }" />
            <div *ngIf="submitted && f.email.errors" class="invalid-feedback">
                <div *ngIf="f.email.errors.required">Email is required</div>
                <div *ngIf="f.email.errors.email">Email is invalid</div>
            </div>
        </div>
        <div class="mb-3">
            <button [disabled]="loading" class="btn btn-primary">
                <span *ngIf="loading" class="spinner-border spinner-border-sm me-1"></span>
                Submit
            </button>
            <a routerLink="../login" class="btn btn-link">Cancel</a>
        </div>
    </form>
</div>
 

Forgot Password Component

Path: /src/app/account/forgot-password.component.ts

On valid submit the forgot password component calls this.accountService.forgotPassword() and displays either a success or error message. If the email matches a registered account the fake backend API displays a password reset "email" on the screen with instructions (a real backend api would send an actual email for this step), the instructions include a link to reset the password of the account.

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { first, finalize } from 'rxjs/operators';

import { AccountService, AlertService } from '@app/_services';

@Component({ templateUrl: 'forgot-password.component.html' })
export class ForgotPasswordComponent implements OnInit {
    form!: FormGroup;
    loading = false;
    submitted = false;

    constructor(
        private formBuilder: FormBuilder,
        private accountService: AccountService,
        private alertService: AlertService
    ) { }

    ngOnInit() {
        this.form = this.formBuilder.group({
            email: ['', [Validators.required, Validators.email]]
        });
    }

    // convenience getter for easy access to form fields
    get f() { return this.form.controls; }

    onSubmit() {
        this.submitted = true;

        // reset alerts on submit
        this.alertService.clear();

        // stop here if form is invalid
        if (this.form.invalid) {
            return;
        }

        this.loading = true;
        this.accountService.forgotPassword(this.f.email.value)
            .pipe(first())
            .pipe(finalize(() => this.loading = false))
            .subscribe({
                next: () => this.alertService.success('Please check your email for password reset instructions'),
                error: error => this.alertService.error(error)
            });
    }
}
 

Account Layout Component Template

Path: /src/app/account/layout.component.html

The account layout component template is the root template of the account feature / section of the boilerplate app, it contains the outer HTML for account registration, authentication and verification pages, and a <router-outlet> for rendering the currently routed component.

<div class="container">
    <div class="row">
        <div class="col-lg-8 offset-lg-2 mt-5">
            <div class="card m-3">
                <router-outlet></router-outlet>
            </div>
        </div>
    </div>
</div>
 

Account Layout Component

Path: /src/app/account/layout.component.ts

The account layout component is the root component of the account feature / section of the boilerplate app, it is bound to the account layout template with the templateUrl property of the angular @Component decorator, and automatically redirects the user to the home page if they are already logged in.

import { Component } from '@angular/core';
import { Router } from '@angular/router';

import { AccountService } from '@app/_services';

@Component({ templateUrl: 'layout.component.html' })
export class LayoutComponent {
    constructor(
        private router: Router,
        private accountService: AccountService
    ) {
        // redirect to home if already logged in
        if (this.accountService.accountValue) {
            this.router.navigate(['/']);
        }
    }
}
 

Login Component Template

Path: /src/app/account/login.component.html

The login component template contains a login form with email and password fields. It displays validation messages for invalid fields when the form is submitted. The form submit event is bound to the onSubmit() method of the login component.

The component uses reactive form validation to validate the input fields, for more information about angular reactive form validation see Angular 14 - Reactive Forms Validation Example.

<h3 class="card-header">Login</h3>
<div class="card-body">
    <form [formGroup]="form" (ngSubmit)="onSubmit()">
        <div class="mb-3">
            <label class="form-label">Email</label>
            <input type="text" formControlName="email" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.email.errors }" />
            <div *ngIf="submitted && f.email.errors" class="invalid-feedback">
                <div *ngIf="f.email.errors.required">Email is required</div>
                <div *ngIf="f.email.errors.email">Email is invalid</div>
            </div>
        </div>
        <div class="mb-3">
            <label class="form-label">Password</label>
            <input type="password" formControlName="password" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.password.errors }" />
            <div *ngIf="submitted && f.password.errors" class="invalid-feedback">
                <div *ngIf="f.password.errors.required">Password is required</div>
            </div>
        </div>
        <div class="row">
            <div class="mb-3 col">
                <button [disabled]="submitting" class="btn btn-primary">
                    <span *ngIf="submitting" class="spinner-border spinner-border-sm me-1"></span>
                    Login
                </button>
                <a routerLink="../register" class="btn btn-link">Register</a>
            </div>
            <div class="mb-3 col text-end">
                <a routerLink="../forgot-password" class="btn btn-link pe-0">Forgot Password?</a>
            </div>
        </div>
    </form>
</div>
 

Login Component

Path: /src/app/account/login.component.ts

The login component uses the account service to login to the application on form submit. It creates the form fields and validators using an Angular FormBuilder to create an instance of a FormGroup that is stored in the form property. The form is then bound to the <form> element in the login component template above using the [formGroup] directive.

On successful login the user is redirected to the page they were trying to access (returnUrl) or the home page ('/') by default. The returnUrl query parameter is added to the url when redirected by the auth guard. On failed login the error returned from the backend is displayed in the UI.

The component contains a convenience getter property f to make it a bit easier to access form controls, for example you can access the password field in the template using f.password instead of form.controls.password.

import { Component, OnInit } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { first } from 'rxjs/operators';

import { AccountService, AlertService } from '@app/_services';

@Component({ templateUrl: 'login.component.html' })
export class LoginComponent implements OnInit {
    form!: FormGroup;
    submitting = false;
    submitted = false;

    constructor(
        private formBuilder: FormBuilder,
        private route: ActivatedRoute,
        private router: Router,
        private accountService: AccountService,
        private alertService: AlertService
    ) { }

    ngOnInit() {
        this.form = this.formBuilder.group({
            email: ['', [Validators.required, Validators.email]],
            password: ['', Validators.required]
        });
    }

    // convenience getter for easy access to form fields
    get f() { return this.form.controls; }

    onSubmit() {
        this.submitted = true;

        // reset alerts on submit
        this.alertService.clear();

        // stop here if form is invalid
        if (this.form.invalid) {
            return;
        }

        this.submitting = true;
        this.accountService.login(this.f.email.value, this.f.password.value)
            .pipe(first())
            .subscribe({
                next: () => {
                    // get return url from query parameters or default to home page
                    const returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/';
                    this.router.navigateByUrl(returnUrl);
                },
                error: error => {
                    this.alertService.error(error);
                    this.submitting = false;
                }
            });
    }
}
 

Register Component Template

Path: /src/app/account/register.component.html

The register component template contains a account registration form with fields for title, first name, last name, email, password, confirm password and an accept Ts & Cs checkbox. All fields are required including the checkbox, the email field must be a valid email address, the password field must have a min length of 6 and must match the confirm password field.

The template displays validation messages for invalid fields when the form is submitted. The form element uses the [formGroup] directive to bind to the form FormGroup in the register component below, and it binds the form submit event to the onSubmit() handler in the register component using the angular event binding (ngSubmit)="onSubmit()".

The component uses reactive form validation to validate the input fields, for more information about angular reactive form validation see Angular 14 - Reactive Forms Validation Example.

<h3 class="card-header">Register</h3>
<div class="card-body">
    <form [formGroup]="form" (ngSubmit)="onSubmit()">
        <div class="row">
            <div class="mb-3 col-2">
                <label class="form-label">Title</label>
                <select formControlName="title" class="form-select" [ngClass]="{ 'is-invalid': submitted && f.title.errors }">
                    <option value=""></option>
                    <option value="Mr">Mr</option>
                    <option value="Mrs">Mrs</option>
                    <option value="Miss">Miss</option>
                    <option value="Ms">Ms</option>
                </select>
                <div *ngIf="submitted && f.title.errors" class="invalid-feedback">
                    <div *ngIf="f.title.errors.required">Title is required</div>
                </div>
            </div>
            <div class="mb-3 col-5">
                <label class="form-label">First Name</label>
                <input type="text" formControlName="firstName" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.firstName.errors }" />
                <div *ngIf="submitted && f.firstName.errors" class="invalid-feedback">
                    <div *ngIf="f.firstName.errors.required">First Name is required</div>
                </div>
            </div>
            <div class="mb-3 col-5">
                <label class="form-label">Last Name</label>
                <input type="text" formControlName="lastName" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.lastName.errors }" />
                <div *ngIf="submitted && f.lastName.errors" class="invalid-feedback">
                    <div *ngIf="f.lastName.errors.required">Last Name is required</div>
                </div>
            </div>
        </div>
        <div class="mb-3">
            <label class="form-label">Email</label>
            <input type="text" formControlName="email" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.email.errors }" />
            <div *ngIf="submitted && f.email.errors" class="invalid-feedback">
                <div *ngIf="f.email.errors.required">Email is required</div>
                <div *ngIf="f.email.errors.email">Email must be a valid email address</div>
            </div>
        </div>
        <div class="row">
            <div class="mb-3 col">
                <label class="form-label">Password</label>
                <input type="password" formControlName="password" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.password.errors }" />
                <div *ngIf="submitted && f.password.errors" class="invalid-feedback">
                    <div *ngIf="f.password.errors.required">Password is required</div>
                    <div *ngIf="f.password.errors.minlength">Password must be at least 6 characters</div>
                </div>
            </div>
            <div class="mb-3 col">
                <label class="form-label">Confirm Password</label>
                <input type="password" formControlName="confirmPassword" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.confirmPassword.errors }" />
                <div *ngIf="submitted && f.confirmPassword.errors" class="invalid-feedback">
                    <div *ngIf="f.confirmPassword.errors.required">Confirm Password is required</div>
                    <div *ngIf="f.confirmPassword.errors.mustMatch">Passwords must match</div>
                </div>
            </div>
        </div>
        <div class="mb-3 form-check">
            <input type="checkbox" formControlName="acceptTerms" id="acceptTerms" class="form-check-input" [ngClass]="{ 'is-invalid': submitted && f.acceptTerms.errors }" />
            <label for="acceptTerms" class="form-check-label">Accept Terms & Conditions</label>
            <div *ngIf="submitted && f.acceptTerms.errors" class="invalid-feedback">Accept Ts & Cs is required</div>
        </div>
        <div class="mb-3">
            <button [disabled]="submitting" class="btn btn-primary">
                <span *ngIf="submitting" class="spinner-border spinner-border-sm me-1"></span>
                Register
            </button>
            <a routerLink="../login" href="" class="btn btn-link">Cancel</a>
        </div>
    </form>
</div>
 

Register Component

Path: /src/app/account/register.component.ts

The register component creates a new account with the account service when the register form is valid and submitted.

It creates the form fields and validators using an Angular FormBuilder to create an instance of a FormGroup that is stored in the form property. The form is then bound to the <form> element in the register component template using the [formGroup] directive.

The component contains a convenience getter property f to make it a bit easier to access form controls, for example you can access the password field in the template using f.password instead of form.controls.password.

On successful registration a success message is displayed and the user is redirected to the login page, then the fake backend API displays a verification "email" on the screen with instructions (a real backend api would send an actual email for this step), the instructions include a link to verify the account.

import { Component, OnInit } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { first } from 'rxjs/operators';

import { AccountService, AlertService } from '@app/_services';
import { MustMatch } from '@app/_helpers';

@Component({ templateUrl: 'register.component.html' })
export class RegisterComponent implements OnInit {
    form!: FormGroup;
    submitting = false;
    submitted = false;

    constructor(
        private formBuilder: FormBuilder,
        private route: ActivatedRoute,
        private router: Router,
        private accountService: AccountService,
        private alertService: AlertService
    ) { }

    ngOnInit() {
        this.form = this.formBuilder.group({
            title: ['', Validators.required],
            firstName: ['', Validators.required],
            lastName: ['', Validators.required],
            email: ['', [Validators.required, Validators.email]],
            password: ['', [Validators.required, Validators.minLength(6)]],
            confirmPassword: ['', Validators.required],
            acceptTerms: [false, Validators.requiredTrue]
        }, {
            validator: MustMatch('password', 'confirmPassword')
        });
    }

    // convenience getter for easy access to form fields
    get f() { return this.form.controls; }

    onSubmit() {
        this.submitted = true;

        // reset alerts on submit
        this.alertService.clear();

        // stop here if form is invalid
        if (this.form.invalid) {
            return;
        }

        this.submitting = true;
        this.accountService.register(this.form.value)
            .pipe(first())
            .subscribe({
                next: () => {
                    this.alertService.success('Registration successful, please check your email for verification instructions', { keepAfterRouteChange: true });
                    this.router.navigate(['../login'], { relativeTo: this.route });
                },
                error: error => {
                    this.alertService.error(error);
                    this.submitting = false;
                }
            });
    }
}
 

Reset Password Component Template

Path: /src/app/account/reset-password.component.html

The reset password component template renders one of the following three views based on the status of the token being validated by the reset password component:

  • Token Validating: a message stating that the token is validating.
  • Token Invalid: a message that the validation failed and a link to the forgot password page to get a new token.
  • Token Valid: a form to reset the password that contains fields for password and confirm password.
<h3 class="card-header">Reset Password</h3>
<div class="card-body">
    <div *ngIf="tokenStatus == TokenStatus.Validating">
        Validating token...
    </div>
    <div *ngIf="tokenStatus == TokenStatus.Invalid">
        Token validation failed, if the token has expired you can get a new one at the <a routerLink="../forgot-password">forgot password</a> page.
    </div>
    <form *ngIf="tokenStatus == TokenStatus.Valid" [formGroup]="form" (ngSubmit)="onSubmit()">
        <div class="mb-3">
            <label class="form-label">Password</label>
            <input type="password" formControlName="password" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.password.errors }" />
            <div *ngIf="submitted && f.password.errors" class="invalid-feedback">
                <div *ngIf="f.password.errors.required">Password is required</div>
                <div *ngIf="f.password.errors.minlength">Password must be at least 6 characters</div>
            </div>
        </div>
        <div class="mb-3">
            <label class="form-label">Confirm Password</label>
            <input type="password" formControlName="confirmPassword" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.confirmPassword.errors }" />
            <div *ngIf="submitted && f.confirmPassword.errors" class="invalid-feedback">
                <div *ngIf="f.confirmPassword.errors.required">Confirm Password is required</div>
                <div *ngIf="f.confirmPassword.errors.mustMatch">Passwords must match</div>
            </div>
        </div>
        <div class="mb-3">
            <button [disabled]="loading" class="btn btn-primary">
                <span *ngIf="loading" class="spinner-border spinner-border-sm me-1"></span>
                Reset Password
            </button>
            <a routerLink="../login" class="btn btn-link">Cancel</a>
        </div>
    </form>
</div>
 

Reset Password Component

Path: /src/app/account/reset-password.component.ts

The reset password component displays a form for resetting an account password when it receives a valid password reset token in the url querystring parameters. The token is validated when the component initializes by calling this.accountService.validateResetToken(token) from the ngOnInit() Angular lifecycle method.

The tokenStatus controls what is rendered by the reset password component template, the initial status is Validating before changing to either Valid or Invalid. The TokenStatus enum is used to set the status so we don't have to use string values.

On form submit the password is reset by calling this.accountService.resetPassword(...) which sends the token and new password to the backend. The backend should validate the token again before updating the password, see the resetPassword() function in the fake backend API for an example.

On successful password reset the user is redirected to the login page with a success message and can login to the boilerplate app with the new password.

import { Component, OnInit } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { first } from 'rxjs/operators';

import { AccountService, AlertService } from '@app/_services';
import { MustMatch } from '@app/_helpers';

enum TokenStatus {
    Validating,
    Valid,
    Invalid
}

@Component({ templateUrl: 'reset-password.component.html' })
export class ResetPasswordComponent implements OnInit {
    TokenStatus = TokenStatus;
    tokenStatus = TokenStatus.Validating;
    token?: string;
    form!: FormGroup;
    loading = false;
    submitted = false;

    constructor(
        private formBuilder: FormBuilder,
        private route: ActivatedRoute,
        private router: Router,
        private accountService: AccountService,
        private alertService: AlertService
    ) { }

    ngOnInit() {
        this.form = this.formBuilder.group({
            password: ['', [Validators.required, Validators.minLength(6)]],
            confirmPassword: ['', Validators.required],
        }, {
            validator: MustMatch('password', 'confirmPassword')
        });

        const token = this.route.snapshot.queryParams['token'];

        // remove token from url to prevent http referer leakage
        this.router.navigate([], { relativeTo: this.route, replaceUrl: true });

        this.accountService.validateResetToken(token)
            .pipe(first())
            .subscribe({
                next: () => {
                    this.token = token;
                    this.tokenStatus = TokenStatus.Valid;
                },
                error: () => {
                    this.tokenStatus = TokenStatus.Invalid;
                }
            });
    }

    // convenience getter for easy access to form fields
    get f() { return this.form.controls; }

    onSubmit() {
        this.submitted = true;

        // reset alerts on submit
        this.alertService.clear();

        // stop here if form is invalid
        if (this.form.invalid) {
            return;
        }

        this.loading = true;
        this.accountService.resetPassword(this.token!, this.f.password.value, this.f.confirmPassword.value)
            .pipe(first())
            .subscribe({
                next: () => {
                    this.alertService.success('Password reset successful, you can now login', { keepAfterRouteChange: true });
                    this.router.navigate(['../login'], { relativeTo: this.route });
                },
                error: error => {
                    this.alertService.error(error);
                    this.loading = false;
                }
            });
    }
}
 

Verify Email Component Template

Path: /src/app/account/verify-email.component.html

The verify email component template renders one of the following two views based on the status of the email token being verified by the verify email component:

  • Verifying: a message stating that the email is verifying.
  • Failed: a message that email verification failed and a link to the forgot password page as an alternative way to verify the email.

On success the user is redirected to the login page

<h3 class="card-header">Verify Email</h3>
<div class="card-body">
    <div *ngIf="emailStatus == EmailStatus.Verifying">
        Verifying...
    </div>
    <div *ngIf="emailStatus == EmailStatus.Failed">
        Verification failed, you can also verify your account using the <a routerLink="forgot-password">forgot password</a> page.
    </div>
</div>
 

Verify Email Component

Path: /src/app/account/verify-email.component.ts

The verify email component is used to verify new accounts before they can login to the boilerplate app. When a new account is registered an email is sent to the user containing a link to this component with a verification token in the querystring parameters. The token from the email link is verified when the component initializes by calling this.accountService.verifyEmail(token) from the ngOnInit() Angular lifecycle method.

On successful verification the user is redirected to the login page with a success message and they can login to the account, if token verification fails an error message is displayed and a link to the forgot password page which can also be used to verify an account.

NOTE: When using the app with the fake backend API the verification "email" is displayed on the screen instructions, a real backend API sends a real email to the user.

import { Component, OnInit } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { first } from 'rxjs/operators';

import { AccountService, AlertService } from '@app/_services';

enum EmailStatus {
    Verifying,
    Failed
}

@Component({ templateUrl: 'verify-email.component.html' })
export class VerifyEmailComponent implements OnInit {
    EmailStatus = EmailStatus;
    emailStatus = EmailStatus.Verifying;

    constructor(
        private route: ActivatedRoute,
        private router: Router,
        private accountService: AccountService,
        private alertService: AlertService
    ) { }

    ngOnInit() {
        const token = this.route.snapshot.queryParams['token'];

        // remove token from url to prevent http referer leakage
        this.router.navigate([], { relativeTo: this.route, replaceUrl: true });

        this.accountService.verifyEmail(token)
            .pipe(first())
            .subscribe({
                next: () => {
                    this.alertService.success('Verification successful, you can now login', { keepAfterRouteChange: true });
                    this.router.navigate(['../login'], { relativeTo: this.route });
                },
                error: () => {
                    this.emailStatus = EmailStatus.Failed;
                }
            });
    }
}
 

Admin » Accounts Routing Module

Path: /src/app/admin/accounts/accounts-routing.module.ts

The accounts routing module defines the routes for the accounts feature module. It includes routes for listing, adding and editing accounts. The add and edit routes are separate but both load the same component (AddEditComponent) which modifies its behaviour based on the route.

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { ListComponent } from './list.component';
import { AddEditComponent } from './add-edit.component';

const routes: Routes = [
    { path: '', component: ListComponent },
    { path: 'add', component: AddEditComponent },
    { path: 'edit/:id', component: AddEditComponent }
];

@NgModule({
    imports: [RouterModule.forChild(routes)],
    exports: [RouterModule]
})
export class AccountsRoutingModule { }
 

Admin » Accounts Feature Module

Path: /src/app/admin/accounts/accounts.module.ts

The accounts module defines the feature module for the accounts section along with metadata about the module. The imports specify which other angular modules are required by this module, and the declarations specify which components belong to this module. For more info on angular feature modules see https://angular.io/guide/feature-modules.

The accounts module is hooked into the Angular auth boilerplate as a child of the admin section via the admin routing module with lazy loading.

import { NgModule } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';

import { AccountsRoutingModule } from './accounts-routing.module';
import { ListComponent } from './list.component';
import { AddEditComponent } from './add-edit.component';

@NgModule({
    imports: [
        CommonModule,
        ReactiveFormsModule,
        AccountsRoutingModule
    ],
    declarations: [
        ListComponent,
        AddEditComponent
    ]
})
export class AccountsModule { }
 

Admin » Accounts Add/Edit Component Template

Path: /src/app/admin/accounts/add-edit.component.html

The accounts add/edit component template contains a dynamic form that supports both adding and editing accounts. The form is in edit mode when there is an account id property in the current route, otherwise it is in add mode.

In edit mode the form is pre-populated with account details fetched from the API and the password field is optional. The dynamic behaviour is implemented in the accounts add/edit component.

<h1>{{title}}</h1>
<form *ngIf="!loading" [formGroup]="form" (ngSubmit)="onSubmit()">
    <div class="row">
        <div class="mb-3 col-2">
            <label class="form-label">Title</label>
            <select formControlName="title" class="form-select" [ngClass]="{ 'is-invalid': submitted && f.title.errors }">
                <option value=""></option>
                <option value="Mr">Mr</option>
                <option value="Mrs">Mrs</option>
                <option value="Miss">Miss</option>
                <option value="Ms">Ms</option>
            </select>
            <div *ngIf="submitted && f.title.errors" class="invalid-feedback">
                <div *ngIf="f.title.errors.required">Title is required</div>
            </div>
        </div>
        <div class="mb-3 col-5">
            <label class="form-label">First Name</label>
            <input type="text" formControlName="firstName" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.firstName.errors }" />
            <div *ngIf="submitted && f.firstName.errors" class="invalid-feedback">
                <div *ngIf="f.firstName.errors.required">First Name is required</div>
            </div>
        </div>
        <div class="mb-3 col-5">
            <label class="form-label">Last Name</label>
            <input type="text" formControlName="lastName" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.lastName.errors }" />
            <div *ngIf="submitted && f.lastName.errors" class="invalid-feedback">
                <div *ngIf="f.lastName.errors.required">Last Name is required</div>
            </div>
        </div>
    </div>
    <div class="row">
        <div class="mb-3 col-7">
            <label class="form-label">Email</label>
            <input type="text" formControlName="email" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.email.errors }" />
            <div *ngIf="submitted && f.email.errors" class="invalid-feedback">
                <div *ngIf="f.email.errors.required">Email is required</div>
                <div *ngIf="f.email.errors.email">Email must be a valid email address</div>
            </div>
        </div>
        <div class="mb-3 col-5">
            <label class="form-label">Role</label>
            <select formControlName="role" class="form-select" [ngClass]="{ 'is-invalid': submitted && f.role.errors }">
                <option value=""></option>
                <option value="User">User</option>
                <option value="Admin">Admin</option>
            </select>
            <div *ngIf="submitted && f.role.errors" class="invalid-feedback">
                <div *ngIf="f.role.errors.required">Role is required</div>
            </div>
        </div>
    </div>
    <div *ngIf="id">
        <h3 class="pt-3">Change Password</h3>
        <p>Leave blank to keep the same password</p>
    </div>
    <div class="row">
        <div class="mb-3 col">
            <label class="form-label">Password</label>
            <input type="password" formControlName="password" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.password.errors }" />
            <div *ngIf="submitted && f.password.errors" class="invalid-feedback">
                <div *ngIf="f.password.errors.required">Password is required</div>
                <div *ngIf="f.password.errors.minlength">Password must be at least 6 characters</div>
            </div>
        </div>
        <div class="mb-3 col">
            <label class="form-label">Confirm Password</label>
            <input type="password" formControlName="confirmPassword" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.confirmPassword.errors }" />
            <div *ngIf="submitted && f.confirmPassword.errors" class="invalid-feedback">
                <div *ngIf="f.confirmPassword.errors.required">Confirm Password is required</div>
                <div *ngIf="f.confirmPassword.errors.mustMatch">Passwords must match</div>
            </div>
        </div>
    </div>
    <div class="mb-3">
        <button [disabled]="submitting" class="btn btn-primary">
            <span *ngIf="submitting" class="spinner-border spinner-border-sm me-1"></span>
            Save
        </button>
        <a routerLink="/admin/accounts" class="btn btn-link">Cancel</a>
    </div>
</form>
<div *ngIf="loading" class="text-center m-5">
    <span class="spinner-border spinner-border-lg align-center"></span>
</div>
 

Admin » Accounts Add/Edit Component

Path: /src/app/admin/accounts/add-edit.component.ts

The accounts add/edit component is used for both adding and editing accounts in the angular tutorial app, the component is in edit mode when there is an account id route parameter, otherwise it is in add mode.

In add mode the password field is required and the form fields are empty by default. In edit mode the password field is optional and the form is pre-populated with the specified account details, which are fetched from the API with the account service.

On submit an account is either created or updated by calling the account service, and on success you are redirected back to the accounts list page with a success message.

import { Component, OnInit } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { first } from 'rxjs/operators';

import { AccountService, AlertService } from '@app/_services';
import { MustMatch } from '@app/_helpers';

@Component({ templateUrl: 'add-edit.component.html' })
export class AddEditComponent implements OnInit {
    form!: FormGroup;
    id?: string;
    title!: string;
    loading = false;
    submitting = false;
    submitted = false;

    constructor(
        private formBuilder: FormBuilder,
        private route: ActivatedRoute,
        private router: Router,
        private accountService: AccountService,
        private alertService: AlertService
    ) { }

    ngOnInit() {
        this.id = this.route.snapshot.params['id'];

        this.form = this.formBuilder.group({
            title: ['', Validators.required],
            firstName: ['', Validators.required],
            lastName: ['', Validators.required],
            email: ['', [Validators.required, Validators.email]],
            role: ['', Validators.required],
            // password only required in add mode
            password: ['', [Validators.minLength(6), ...(!this.id ? [Validators.required] : [])]],
            confirmPassword: ['']
        }, {
            validator: MustMatch('password', 'confirmPassword')
        });

        this.title = 'Create Account';
        if (this.id) {
            // edit mode
            this.title = 'Edit Account';
            this.loading = true;
            this.accountService.getById(this.id)
                .pipe(first())
                .subscribe(x => {
                    this.form.patchValue(x);
                    this.loading = false;
                });
        }
    }

    // convenience getter for easy access to form fields
    get f() { return this.form.controls; }

    onSubmit() {
        this.submitted = true;

        // reset alerts on submit
        this.alertService.clear();

        // stop here if form is invalid
        if (this.form.invalid) {
            return;
        }

        this.submitting = true;

        // create or update account based on id param
        let saveAccount;
        let message: string;
        if (this.id) {
            saveAccount = () => this.accountService.update(this.id!, this.form.value);
            message = 'Account updated';
        } else {
            saveAccount = () => this.accountService.create(this.form.value);
            message = 'Account created';
        }

        saveAccount()
            .pipe(first())
            .subscribe({
                next: () => {
                    this.alertService.success(message, { keepAfterRouteChange: true });
                    this.router.navigateByUrl('/admin/accounts');
                },
                error: error => {
                    this.alertService.error(error);
                    this.submitting = false;
                }
            });
    }
}
 

Admin » Accounts List Component Template

Path: /src/app/admin/accounts/list.component.html

The accounts list component template displays a list of all accounts and contains buttons for creating, editing and deleting accounts.

<h1>Accounts</h1>
<p>All accounts from secure (admin only) api end point:</p>
<a routerLink="add" class="btn btn-sm btn-success mb-2">Create Account</a>
<table class="table table-striped">
    <thead>
        <tr>
            <th style="width:30%">Name</th>
            <th style="width:30%">Email</th>
            <th style="width:30%">Role</th>
            <th style="width:10%"></th>
        </tr>
    </thead>
    <tbody>
        <tr *ngFor="let account of accounts">
            <td class="align-middle">{{account.title}} {{account.firstName}} {{account.lastName}}</td>
            <td class="align-middle">{{account.email}}</td>
            <td class="align-middle">{{account.role}}</td>
            <td style="white-space: nowrap">
                <a routerLink="edit/{{account.id}}" class="btn btn-sm btn-primary me-1">Edit</a>
                <button (click)="deleteAccount(account.id)" class="btn btn-sm btn-danger btn-delete-account" [disabled]="account.isDeleting">
                    <span *ngIf="account.isDeleting" class="spinner-border spinner-border-sm"></span>
                    <span *ngIf="!account.isDeleting">Delete</span>
                </button>
            </td>
        </tr>
        <tr *ngIf="!accounts">
            <td colspan="4" class="text-center">
                <span class="spinner-border spinner-border-lg align-center"></span>
            </td>
        </tr>
    </tbody>
</table>
 

Admin » Accounts List Component

Path: /src/app/admin/accounts/list.component.ts

The accounts list component gets all accounts from the account service in the ngOnInit() method and makes them available to the accounts list template via the accounts property.

The deleteAccount() method sets the property account.isDeleting = true so the template displays a spinner on the delete button, then calls this.accountService.delete(id) to delete the account from the API and removes the deleted account from component accounts array so it is removed from the UI.

import { Component, OnInit } from '@angular/core';
import { first } from 'rxjs/operators';

import { AccountService } from '@app/_services';

@Component({ templateUrl: 'list.component.html' })
export class ListComponent implements OnInit {
    accounts?: any[];

    constructor(private accountService: AccountService) { }

    ngOnInit() {
        this.accountService.getAll()
            .pipe(first())
            .subscribe(accounts => this.accounts = accounts);
    }

    deleteAccount(id: string) {
        const account = this.accounts!.find(x => x.id === id);
        account.isDeleting = true;
        this.accountService.delete(id)
            .pipe(first())
            .subscribe(() => {
                this.accounts = this.accounts!.filter(x => x.id !== id)
            });
    }
}
 

Admin Routing Module

Path: /src/app/admin/admin-routing.module.ts

The admin routing module defines the routes for the admin feature module. There are routes for the overview page and accounts section, and a parent route for the admin layout component which contains the common layout code for the admin section.

The SubNavComponent route renders the admin subnav component in the 'subnav' outlet of the app component template for all admin pages. The subnav outlet is located directly below the main nav in the app component template.

The accounts feature module is a lazy loaded as a child module of the admin feature module.

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { SubNavComponent } from './subnav.component';
import { LayoutComponent } from './layout.component';
import { OverviewComponent } from './overview.component';

const accountsModule = () => import('./accounts/accounts.module').then(x => x.AccountsModule);

const routes: Routes = [
    { path: '', component: SubNavComponent, outlet: 'subnav' },
    {
        path: '', component: LayoutComponent,
        children: [
            { path: '', component: OverviewComponent },
            { path: 'accounts', loadChildren: accountsModule }
        ]
    }
];

@NgModule({
    imports: [RouterModule.forChild(routes)],
    exports: [RouterModule]
})
export class AdminRoutingModule { }
 

Admin Feature Module

Path: /src/app/admin/admin.module.ts

The admin module defines the feature module for the admin section along with metadata about the module. The imports specify which other angular modules are required by this module, and the declarations specify which components belong to this module. For more info on angular feature modules see https://angular.io/guide/feature-modules.

The admin module is hooked into the main app inside the app routing module with lazy loading.

import { NgModule } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';

import { AdminRoutingModule } from './admin-routing.module';
import { SubNavComponent } from './subnav.component';
import { LayoutComponent } from './layout.component';
import { OverviewComponent } from './overview.component';

@NgModule({
    imports: [
        CommonModule,
        ReactiveFormsModule,
        AdminRoutingModule
    ],
    declarations: [
        SubNavComponent,
        LayoutComponent,
        OverviewComponent
    ]
})
export class AdminModule { }
 

Admin Layout Component Template

Path: /src/app/admin/layout.component.html

The admin layout component template is the root template of the admin feature / section of the app, it contains the outer HTML for all /admin pages and a <router-outlet> for rendering the currently routed component.

<div class="p-4">
    <div class="container">
        <router-outlet></router-outlet>
    </div>
</div>
 

Admin Layout Component

Path: /src/app/admin/layout.component.ts

The admin layout component is the root component of the admin feature / section of the boilerplate app, it is bound to the admin layout template with the templateUrl property of the angular @Component decorator.

import { Component } from '@angular/core';

@Component({ templateUrl: 'layout.component.html' })
export class LayoutComponent { }
 

Admin Overview Component Template

Path: /src/app/admin/overview.component.html

The admin overview component template displays some basic info about the admin section and a link to the "accounts" subsection.

<div class="p-4">
    <div class="container">
        <h1>Admin</h1>
        <p>This section can only be accessed by administrators.</p>
        <p><a routerLink="accounts">Manage Accounts</a></p>
    </div>
</div>
 

Admin Overview Component

Path: /src/app/admin/overview.component.ts

The admin overview component is the default component of the admin section of the boilerplate app, it is bound to the admin overview template with the templateUrl property of the angular @Component decorator.

import { Component } from '@angular/core';

@Component({ templateUrl: 'overview.component.html' })
export class OverviewComponent { }
 

Admin Sub Nav Component Template

Path: /src/app/admin/subnav.component.html

The admin sub nav component template contains the navigation for the admin section of the boilerplate app, it is displayed directly below the main nav on all admin pages of the application.

The sub nav is rendered in the "subnav" router outlet of the app component template by the admin routing module.

<nav class="admin-nav navbar navbar-expand navbar-light px-3">
    <div class="navbar-nav">
        <a routerLink="accounts" routerLinkActive="active" class="nav-item nav-link">Accounts</a>
    </div>
</nav>
 

Admin Sub Nav Component

Path: /src/app/admin/subnav.component.ts

The admin sub nav component contains the class for the admin sub nav template, it is bound to the template with the templateUrl property of the angular @Component decorator.

import { Component } from '@angular/core';

@Component({ templateUrl: 'subnav.component.html' })
export class SubNavComponent { }
 

Home Component Template

Path: /src/app/home/home.component.html

The home component template displays a simple welcome message with the first name of the logged in account.

<div class="p-4">
    <div class="container">
        <h1>Hi {{account?.firstName}}!</h1>
        <p>You're logged in with Angular 15 & JWT!!</p>
    </div>
</div>
 

Home Component

Path: /src/app/home/home.component.ts

The home component defines an angular component that gets the current logged in account from the account service and makes it available to the home template via the account property.

import { Component } from '@angular/core';

import { AccountService } from '@app/_services';

@Component({ templateUrl: 'home.component.html' })
export class HomeComponent {
    account = this.accountService.accountValue;

    constructor(private accountService: AccountService) { }
}
 

Profile Details Component Template

Path: /src/app/profile/details.component.html

The profile details component template displays the name and email of the authenticated account with a link to the update profile page.

<h1>My Profile</h1>
<p *ngIf="account">
    <strong>Name: </strong> {{account.title}} {{account.firstName}} {{account.lastName}}<br />
    <strong>Email: </strong> {{account.email}}
</p>
<p><a routerLink="update">Update Profile</a></p>
 

Profile Details Component

Path: /src/app/profile/details.component.ts

The profile details component defines an angular component that gets the current logged in account from the account service and makes it available to the profile details template via the account property.

import { Component } from '@angular/core';

import { AccountService } from '@app/_services';

@Component({ templateUrl: 'details.component.html' })
export class DetailsComponent {
    account = this.accountService.accountValue;

    constructor(private accountService: AccountService) { }
}
 

Profile Layout Component Template

Path: /src/app/profile/layout.component.html

The profile layout component template is the root template of the profile feature / section of the boilerplate app, it contains the outer HTML for all /profile pages and a <router-outlet> to render the currently routed component.

<div class="p-4">
    <div class="container">
        <router-outlet></router-outlet>
    </div>
</div>
 

Profile Layout Component

Path: /src/app/profile/layout.component.ts

The profile layout component is the root component of the profile feature / section of the boilerplate app, it is bound to the profile layout template with the templateUrl property of the angular @Component decorator.

import { Component } from '@angular/core';

@Component({ templateUrl: 'layout.component.html' })
export class LayoutComponent { }
 

Profile Routing Module

Path: /src/app/profile/profile-routing.module.ts

The profile routing module defines the routes for the profile feature module. It includes routes for profile details and update profile, and a parent route for the profile layout which contains the common layout code for the profile section.

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { LayoutComponent } from './layout.component';
import { DetailsComponent } from './details.component';
import { UpdateComponent } from './update.component';

const routes: Routes = [
    {
        path: '', component: LayoutComponent,
        children: [
            { path: '', component: DetailsComponent },
            { path: 'update', component: UpdateComponent }
        ]
    }
];

@NgModule({
    imports: [RouterModule.forChild(routes)],
    exports: [RouterModule]
})
export class ProfileRoutingModule { }
 

Profile Feature Module

Path: /src/app/profile/profile.module.ts

The profile module defines the feature module for the profile section of the angular boilerplate app. The imports specify which other angular modules are required by this module and the declarations specify which components belong to this module. For more info on angular feature modules see https://angular.io/guide/feature-modules.

The profile module is hooked into the main app inside the app routing module with lazy loading.

import { NgModule } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';

import { ProfileRoutingModule } from './profile-routing.module';
import { LayoutComponent } from './layout.component';
import { DetailsComponent } from './details.component';
import { UpdateComponent } from './update.component';

@NgModule({
    imports: [
        CommonModule,
        ReactiveFormsModule,
        ProfileRoutingModule
    ],
    declarations: [
        LayoutComponent,
        DetailsComponent,
        UpdateComponent
    ]
})
export class ProfileModule { }
 

Profile Update Component Template

Path: /src/app/profile/update.component.html

The profile update component template contains a form for updating the details of the currently authenticated account, changing password, or deleting the account.

<h1>Update Profile</h1>
<form [formGroup]="form" (ngSubmit)="onSubmit()">
    <div class="row">
        <div class="mb-3 col-2">
            <label class="form-label">Title</label>
            <select formControlName="title" class="form-select" [ngClass]="{ 'is-invalid': submitted && f.title.errors }">
                <option value=""></option>
                <option value="Mr">Mr</option>
                <option value="Mrs">Mrs</option>
                <option value="Miss">Miss</option>
                <option value="Ms">Ms</option>
            </select>
            <div *ngIf="submitted && f.title.errors" class="invalid-feedback">
                <div *ngIf="f.title.errors.required">Title is required</div>
            </div>
        </div>
        <div class="mb-3 col-5">
            <label class="form-label">First Name</label>
            <input type="text" formControlName="firstName" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.firstName.errors }" />
            <div *ngIf="submitted && f.firstName.errors" class="invalid-feedback">
                <div *ngIf="f.firstName.errors.required">First Name is required</div>
            </div>
        </div>
        <div class="mb-3 col-5">
            <label class="form-label">Last Name</label>
            <input type="text" formControlName="lastName" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.lastName.errors }" />
            <div *ngIf="submitted && f.lastName.errors" class="invalid-feedback">
                <div *ngIf="f.lastName.errors.required">Last Name is required</div>
            </div>
        </div>
    </div>
    <div class="mb-3">
        <label class="form-label">Email</label>
        <input type="text" formControlName="email" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.email.errors }" />
        <div *ngIf="submitted && f.email.errors" class="invalid-feedback">
            <div *ngIf="f.email.errors.required">Email is required</div>
            <div *ngIf="f.email.errors.email">Email must be a valid email address</div>
        </div>
    </div>
    <h3 class="pt-3">Change Password</h3>
    <p>Leave blank to keep the same password</p>
    <div class="row">
        <div class="mb-3 col">
            <label class="form-label">Password</label>
            <input type="password" formControlName="password" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.password.errors }" />
            <div *ngIf="submitted && f.password.errors" class="invalid-feedback">
                <div *ngIf="f.password.errors.required">Password is required</div>
                <div *ngIf="f.password.errors.minlength">Password must be at least 6 characters</div>
            </div>
        </div>
        <div class="mb-3 col">
            <label class="form-label">Confirm Password</label>
            <input type="password" formControlName="confirmPassword" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.confirmPassword.errors }" />
            <div *ngIf="submitted && f.confirmPassword.errors" class="invalid-feedback">
                <div *ngIf="f.confirmPassword.errors.required">Confirm Password is required</div>
                <div *ngIf="f.confirmPassword.errors.mustMatch">Passwords must match</div>
            </div>
        </div>
    </div>
    <div class="mb-3">
        <button type="submit" [disabled]="submitting" class="btn btn-primary me-2">
            <span *ngIf="submitting" class="spinner-border spinner-border-sm me-1"></span>
            Update
        </button>
        <button type="button" (click)="onDelete()" [disabled]="deleting" class="btn btn-danger">
            <span *ngIf="deleting" class="spinner-border spinner-border-sm me-1"></span>
            Delete
        </button>
        <a routerLink="../" href="" class="btn btn-link">Cancel</a>
    </div>
</form>
 

Profile Update Component

Path: /src/app/profile/update.component.ts

The profile update component enables the current user to update their profile, change their password, or delete their account. It pre-populates the form fields with the current account from the account service (this.accountService.accountValue), and is bound to the profile update template with the templateUrl property of the angular @Component decorator.

On successful update the user is redirected back to the profile details page with a success message. On successful delete the user is logged out of the boilerplate app with a message that the account was successully deleted.

import { Component, OnInit } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { first } from 'rxjs/operators';

import { AccountService, AlertService } from '@app/_services';
import { MustMatch } from '@app/_helpers';

@Component({ templateUrl: 'update.component.html' })
export class UpdateComponent implements OnInit {
    account = this.accountService.accountValue!;
    form!: FormGroup;
    submitting = false;
    submitted = false;
    deleting = false;

    constructor(
        private formBuilder: FormBuilder,
        private route: ActivatedRoute,
        private router: Router,
        private accountService: AccountService,
        private alertService: AlertService
    ) { }

    ngOnInit() {
        this.form = this.formBuilder.group({
            title: [this.account.title, Validators.required],
            firstName: [this.account.firstName, Validators.required],
            lastName: [this.account.lastName, Validators.required],
            email: [this.account.email, [Validators.required, Validators.email]],
            password: ['', [Validators.minLength(6)]],
            confirmPassword: ['']
        }, {
            validator: MustMatch('password', 'confirmPassword')
        });
    }

    // convenience getter for easy access to form fields
    get f() { return this.form.controls; }

    onSubmit() {
        this.submitted = true;

        // reset alerts on submit
        this.alertService.clear();

        // stop here if form is invalid
        if (this.form.invalid) {
            return;
        }

        this.submitting = true;
        this.accountService.update(this.account.id!, this.form.value)
            .pipe(first())
            .subscribe({
                next: () => {
                    this.alertService.success('Update successful', { keepAfterRouteChange: true });
                    this.router.navigate(['../'], { relativeTo: this.route });
                },
                error: error => {
                    this.alertService.error(error);
                    this.submitting = false;
                }
            });
    }

    onDelete() {
        if (confirm('Are you sure?')) {
            this.deleting = true;
            this.accountService.delete(this.account.id!)
                .pipe(first())
                .subscribe(() => {
                    this.alertService.success('Account deleted successfully', { keepAfterRouteChange: true });
                });
        }
    }
}
 

App Routing Module

Path: /src/app/app-routing.module.ts

The app routing module defines the top level routes for the boilerplate angular application and generates a root routing module by passing the array of routes to the RouterModule.forRoot() method. The module is imported into the main app module below.

The home route maps the root path (/) of the app to the home component, the /account route maps to the account feature module, the /profile route maps to the profile feature module and the /admin route maps to the admin feature module, all three feature module routes (account, profile, admin) are lazy loaded.

The home, profile and admin routes are secured by passing the auth guard to the canActivate property of each route, and the admin route is restricted to accounts with the Admin role by configuring the route with data: { roles: [Role.Admin] }.

For more information on angular routing see https://angular.io/guide/router.

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { HomeComponent } from './home';
import { AuthGuard } from './_helpers';
import { Role } from './_models';

const accountModule = () => import('./account/account.module').then(x => x.AccountModule);
const adminModule = () => import('./admin/admin.module').then(x => x.AdminModule);
const profileModule = () => import('./profile/profile.module').then(x => x.ProfileModule);

const routes: Routes = [
    { path: '', component: HomeComponent, canActivate: [AuthGuard] },
    { path: 'account', loadChildren: accountModule },
    { path: 'profile', loadChildren: profileModule, canActivate: [AuthGuard] },
    { path: 'admin', loadChildren: adminModule, canActivate: [AuthGuard], data: { roles: [Role.Admin] } },

    // otherwise redirect to home
    { path: '**', redirectTo: '' }
];

@NgModule({
    imports: [RouterModule.forRoot(routes)],
    exports: [RouterModule]
})
export class AppRoutingModule { }
 

App Component Template

Path: /src/app/app.component.html

The app component template is the root component template of the boilerplate application, it contains the following:

  • Main nav bar which is only displayed for authenticated accounts.
  • Named router-outlet below the main nav for rendering a "subnav".
  • Global alert component.
  • Main router-outlet for rendering the component that is mapped to the current route.
<div class="app-container" [ngClass]="{ 'bg-light': account }">
    <!-- main nav -->
    <nav class="navbar navbar-expand navbar-dark bg-dark px-3" *ngIf="account">
        <div class="navbar-nav">
            <a routerLink="/" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" class="nav-item nav-link">Home</a>
            <a routerLink="/profile" routerLinkActive="active" class="nav-item nav-link">Profile</a>
            <a *ngIf="account.role === Role.Admin" routerLink="/admin" routerLinkActive="active" class="nav-item nav-link">Admin</a>
            <button (click)="logout()" class="btn btn-link nav-item nav-link">Logout</button>
        </div>
    </nav>

    <!-- subnav router outlet -->
    <router-outlet name="subnav"></router-outlet>

    <!-- global alert -->
    <alert></alert>

    <!-- main router outlet -->
    <router-outlet></router-outlet>
</div>
 

App Component

Path: /src/app/app.component.ts

The app component is the root component of the boilerplate angular app, it defines the root tag of the app as <app-root></app-root> with the selector property of the @Component() decorator.

It subscribes to the account observable of the account service so it can reactively show/hide the main nav bar in the app component template when the user logs in/out of the application. Usually it's best practice to unsubscribe when the component is destroyed to avoid memory leaks. However this isn't necessary in the root app component because it will only be destroyed when the entire app is closed.

The logout() method is called from the logout link in the main nav bar to log the account out and redirect to the login page.

import { Component } from '@angular/core';

import { AccountService } from './_services';
import { Account, Role } from './_models';

@Component({ selector: 'app-root', templateUrl: 'app.component.html' })
export class AppComponent {
    Role = Role;
    account?: Account | null;

    constructor(private accountService: AccountService) {
        this.accountService.account.subscribe(x => this.account = x);
    }

    logout() {
        this.accountService.logout();
    }
}
 

App Module

Path: /src/app/app.module.ts

The app module defines the root module of the angular boilerplate application along with metadata about the module. For more info about angular modules see https://angular.io/guide/ngmodules.

This is where the fake backend provider is added to the application, to switch to a real backend simply remove the fakeBackendProvider located below the comment // provider used to create fake backend.

import { NgModule, APP_INITIALIZER } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { ReactiveFormsModule } from '@angular/forms';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';

// used to create fake backend
import { fakeBackendProvider } from './_helpers';

import { AppRoutingModule } from './app-routing.module';
import { JwtInterceptor, ErrorInterceptor, appInitializer } from './_helpers';
import { AccountService } from './_services';
import { AppComponent } from './app.component';
import { AlertComponent } from './_components';
import { HomeComponent } from './home';

@NgModule({
    imports: [
        BrowserModule,
        ReactiveFormsModule,
        HttpClientModule,
        AppRoutingModule
    ],
    declarations: [
        AppComponent,
        AlertComponent,
        HomeComponent
    ],
    providers: [
        { provide: APP_INITIALIZER, useFactory: appInitializer, multi: true, deps: [AccountService] },
        { provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true },
        { provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true },

        // provider used to create fake backend
        fakeBackendProvider
    ],
    bootstrap: [AppComponent]
})
export class AppModule { }
 

Production Environment Config

Path: /src/environments/environment.prod.ts

The production environment config contains variables required to run the application in production. This enables you to build the application with a different configuration for each different environment (e.g. production & development) without updating the app code.

When you build the application for production with the command ng build --configuration production, the output environment.ts is replaced with environment.prod.ts.

export const environment = {
    production: true,
    apiUrl: 'http://localhost:4000'
};
 

Development Environment Config

Path: /src/environments/environment.ts

The development environment config contains variables required to run the boilerplate app in development.

Environment config is accessed by importing the environment object into any Angular service of component with the line import { environment } from '@environments/environment' and accessing properties on the environment object, see the account service for an example.

export const environment = {
    production: false,
    apiUrl: 'http://localhost:4000'
};
 

Main Index Html File

Path: /src/index.html

The main index.html file is the initial page loaded by the browser that kicks everything off. The Angular CLI (with Webpack under the hood) bundles all of the compiled javascript files together and injects them into the body of the index.html page so the scripts can be loaded and executed by the browser.

<!DOCTYPE html>
<html>
<head>
    <base href="/" />
    <title>Angular 15 Auth Boilerplate - Sign Up with Verification, Login and Forgot Password</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <!-- bootstrap css -->
    <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    <app-root></app-root>
</body>
</html>
 

Main (Bootstrap) File

Path: /src/main.ts

The main file is the entry point used by angular to launch and bootstrap the application.

import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { AppModule } from './app/app.module';
import { environment } from './environments/environment';

if (environment.production) {
    enableProdMode();
}

platformBrowserDynamic().bootstrapModule(AppModule)
    .catch(err => console.error(err));
 

Polyfills

Path: /src/polyfills.ts

Some features used by Angular are not yet supported natively by all major browsers, polyfills are used to add support for features where necessary so your Angular boilerplate application works across all major browsers.

This file is generated by the Angular CLI when creating a new project with the ng new command, I've excluded the comments in the file for brevity.

import 'zone.js';  // Included with Angular CLI.
 

Global LESS/CSS Styles

Path: /src/styles.less

The global styles file contains LESS/CSS styles that are applied globally throughout the angular boilerplate application.

/* You can add global styles to this file, and also import other style files */
.app-container {
    min-height: 320px;
}

.admin-nav {
    padding-top: 0;
    padding-bottom: 0;
    background-color: #e8e9ea;
    border-bottom: 1px solid #ccc;
}

.btn-delete-account {
    width: 40px;
    text-align: center;
    box-sizing: content-box;
}
 

Package.json

Path: /package.json

The package.json file contains project configuration information including package dependencies that get installed when you run npm install and scripts that are executed when you run npm start or npm run build etc. Full documentation is available at https://docs.npmjs.com/files/package.json.

{
    "name": "angular-15-example",
    "version": "0.0.0",
    "scripts": {
        "ng": "ng",
        "start": "ng serve --open",
        "build": "ng build",
        "watch": "ng build --watch --configuration development",
        "test": "ng test"
    },
    "private": true,
    "dependencies": {
        "@angular/animations": "^15.0.0",
        "@angular/common": "^15.0.0",
        "@angular/compiler": "^15.0.0",
        "@angular/core": "^15.0.0",
        "@angular/forms": "^15.0.0",
        "@angular/platform-browser": "^15.0.0",
        "@angular/platform-browser-dynamic": "^15.0.0",
        "@angular/router": "^15.0.0",
        "rxjs": "~7.5.0",
        "tslib": "^2.3.0",
        "zone.js": "~0.12.0"
    },
    "devDependencies": {
        "@angular-devkit/build-angular": "^15.0.1",
        "@angular/cli": "~15.0.1",
        "@angular/compiler-cli": "^15.0.0",
        "@types/jasmine": "~4.3.0",
        "jasmine-core": "~4.5.0",
        "karma": "~6.4.0",
        "karma-chrome-launcher": "~3.1.0",
        "karma-coverage": "~2.2.0",
        "karma-jasmine": "~5.1.0",
        "karma-jasmine-html-reporter": "~2.0.0",
        "typescript": "~4.8.2"
    }
}
 

TypeScript tsconfig.json

Path: /tsconfig.json

The tsconfig.json file contains the base TypeScript compiler configuration for all projects in the Angular workspace, it configures how the TypeScript code will be compiled / transpiled into JavaScript that is understood by the browser. For more info see https://angular.io/guide/typescript-configuration.

Most of the file is unchanged from when it was generated by the Angular CLI, only the paths property has been added to map the @app and @environments aliases to the /src/app and /src/environments directories. This allows imports to be relative to the app and environments folders by prefixing import paths with aliases instead of having to use long relative paths (e.g. import MyComponent from '@app/MyComponent' instead of import MyComponent from '../../../MyComponent').

/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
    "compileOnSave": false,
    "compilerOptions": {
        "baseUrl": "./",
        "outDir": "./dist/out-tsc",
        "allowSyntheticDefaultImports": true,
        "forceConsistentCasingInFileNames": true,
        "strict": true,
        "noImplicitOverride": true,
        "noPropertyAccessFromIndexSignature": false,
        "noImplicitReturns": true,
        "noFallthroughCasesInSwitch": true,
        "sourceMap": true,
        "declaration": false,
        "downlevelIteration": true,
        "experimentalDecorators": true,
        "moduleResolution": "node",
        "importHelpers": true,
        "target": "es2020",
        "module": "es2020",
        "lib": [
            "es2020",
            "dom"
        ],
        "paths": {
            "@app/*": ["src/app/*"],
            "@environments/*": ["src/environments/*"]
        }
    },
    "angularCompilerOptions": {
        "enableI18nLegacyMessageIdFormat": false,
        "strictInjectionParameters": true,
        "strictInputAccessModifiers": true,
        "strictTemplates": true
    }
}


Other versions of this tutorial

The auth boilerplate tutorial is also available in the following versions:

 


Need Some Angular 15 Help?

Search fiverr for freelance Angular 15 developers.


Follow me for updates

On Twitter or RSS.


When I'm not coding...

Me and Tina are on a motorcycle adventure around Australia.
Come along for the ride!


Comments


Supported by