跳转到主要内容

标签(标签)

资源精选(342) Go开发(108) Go语言(103) Go(99) angular(82) LLM(75) 大语言模型(63) 人工智能(53) 前端开发(50) LangChain(43) golang(43) 机器学习(39) Go工程师(38) Go程序员(38) Go开发者(36) React(33) Go基础(29) Python(24) Vue(22) Web开发(20) Web技术(19) 精选资源(19) 深度学习(19) Java(18) ChatGTP(17) Cookie(16) android(16) 前端框架(13) JavaScript(13) Next.js(12) 安卓(11) 聊天机器人(10) typescript(10) 资料精选(10) NLP(10) 第三方Cookie(9) Redwoodjs(9) LLMOps(9) Go语言中级开发(9) 自然语言处理(9) PostgreSQL(9) 区块链(9) mlops(9) 安全(9) 全栈开发(8) ChatGPT(8) OpenAI(8) Linux(8) AI(8) GraphQL(8) iOS(8) 软件架构(7) Go语言高级开发(7) AWS(7) C++(7) 数据科学(7) whisper(6) Prisma(6) 隐私保护(6) RAG(6) JSON(6) DevOps(6) 数据可视化(6) wasm(6) 计算机视觉(6) 算法(6) Rust(6) 微服务(6) 隐私沙盒(5) FedCM(5) 语音识别(5) Angular开发(5) 快速应用开发(5) 提示工程(5) Agent(5) LLaMA(5) 低代码开发(5) Go测试(5) gorm(5) REST API(5) 推荐系统(5) WebAssembly(5) GameDev(5) CMS(5) CSS(5) machine-learning(5) 机器人(5) 游戏开发(5) Blockchain(5) Web安全(5) Kotlin(5) 低代码平台(5) 机器学习资源(5) Go资源(5) Nodejs(5) PHP(5) Swift(5) 智能体(4) devin(4) Blitz(4) javascript框架(4) Redwood(4) GDPR(4) 生成式人工智能(4) Angular16(4) Alpaca(4) 编程语言(4) SAML(4) JWT(4) JSON处理(4) Go并发(4) kafka(4) 移动开发(4) 移动应用(4) security(4) 隐私(4) spring-boot(4) 物联网(4) nextjs(4) 网络安全(4) API(4) Ruby(4) 信息安全(4) flutter(4) 专家智能体(3) Chrome(3) CHIPS(3) 3PC(3) SSE(3) 人工智能软件工程师(3) LLM Agent(3) Remix(3) Ubuntu(3) GPT4All(3) 软件开发(3) 问答系统(3) 开发工具(3) 最佳实践(3) RxJS(3) SSR(3) Node.js(3) Dolly(3) 移动应用开发(3) 低代码(3) IAM(3) Web框架(3) CORS(3) 基准测试(3) Go语言数据库开发(3) Oauth2(3) 并发(3) 主题(3) Theme(3) earth(3) nginx(3) 软件工程(3) azure(3) keycloak(3) 生产力工具(3) gpt3(3) 工作流(3) C(3) jupyter(3) 认证(3) prometheus(3) GAN(3) Spring(3) 逆向工程(3) 应用安全(3) Docker(3) Django(3) R(3) .NET(3) 大数据(3) Hacking(3) 渗透测试(3) C++资源(3) Mac(3) 微信小程序(3) Python资源(3) JHipster(3) 大型语言模型(2) 语言模型(2) 可穿戴设备(2) JDK(2) SQL(2) Apache(2) Hashicorp Vault(2) Spring Cloud Vault(2) Go语言Web开发(2) Go测试工程师(2) WebSocket(2) 容器化(2) AES(2) 加密(2) 输入验证(2) ORM(2) Fiber(2) Postgres(2) Gorilla Mux(2) Go数据库开发(2) 模块(2) 泛型(2) 指针(2) HTTP(2) PostgreSQL开发(2) Vault(2) K8s(2) Spring boot(2) R语言(2) 深度学习资源(2) 半监督学习(2) semi-supervised-learning(2) architecture(2) 普罗米修斯(2) 嵌入模型(2) productivity(2) 编码(2) Qt(2) 前端(2) Rust语言(2) NeRF(2) 神经辐射场(2) 元宇宙(2) CPP(2) 数据分析(2) spark(2) 流处理(2) Ionic(2) 人体姿势估计(2) human-pose-estimation(2) 视频处理(2) deep-learning(2) kotlin语言(2) kotlin开发(2) burp(2) Chatbot(2) npm(2) quantum(2) OCR(2) 游戏(2) game(2) 内容管理系统(2) MySQL(2) python-books(2) pentest(2) opengl(2) IDE(2) 漏洞赏金(2) Web(2) 知识图谱(2) PyTorch(2) 数据库(2) reverse-engineering(2) 数据工程(2) swift开发(2) rest(2) robotics(2) ios-animation(2) 知识蒸馏(2) 安卓开发(2) nestjs(2) solidity(2) 爬虫(2) 面试(2) 容器(2) C++精选(2) 人工智能资源(2) Machine Learning(2) 备忘单(2) 编程书籍(2) angular资源(2) 速查表(2) cheatsheets(2) SecOps(2) mlops资源(2) R资源(2) DDD(2) 架构设计模式(2) 量化(2) Hacking资源(2) 强化学习(2) flask(2) 设计(2) 性能(2) Sysadmin(2) 系统管理员(2) Java资源(2) 机器学习精选(2) android资源(2) android-UI(2) Mac资源(2) iOS资源(2) Vue资源(2) flutter资源(2) JavaScript精选(2) JavaScript资源(2) Rust开发(2) deeplearning(2) RAD(2)

Tutorial built with Angular 14.2.12

Other versions available:

In this tutorial we'll go through an example of how to implement role based authorization / access control in Angular 14. The example builds on a previous tutorial I posted which focuses on JWT authentication, this example has been extended to include role based access control on top of the JWT auth.

Example Angular 14 App

The tutorial example is pretty minimal and contains just 3 pages to demonstrate role based authorization in Angular 14:

  • /login - public login page with username and password fields, on submit the page sends a POST request to the API to authenticate user credentials, on success the API returns a JWT token to make authenticated requests to secure API routes.
  • / - secure home page that is accessible to all roles (User and Admin), it displays a welcome message, the current user role and the current user details fetched from a secure API endpoint.
  • /admin - secure admin page accessible only to the Admin role, it displays a list of all users fetched from a secure API endpoint which is also only accessible to admin users.

Login Form with Angular Reactive Forms

The login form in the example is built with the Reactive Forms library that comes as part of the Angular framework (in @angular/forms). It uses a model-driven approach to build, validate and handle forms in Angular. For a more detailed reactive forms tutorial see Angular 14 - Reactive Forms Validation Example.

Shared State with RxJS

RxJS Subjects and Observables are used to manage shared state in the Angular application. For example the current logged in user is managed by the authentication service with an RxJS BehaviorSubject and exposed to the rest of the app with an RxJS Observable that any Angular component can subscribe to and be notified when the user logs in or out.

Fake Backend API

The Angular app runs with a fake backend by default to enable it to run completely in the browser without a real backend api (backend-less), to switch to a real api simply remove or comment out the line below the comment // provider used to create fake backend located in the app module (/src/app/app.module.ts).

You can build your own API or hook it up with a .NET API or Node.js API available below.

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 tutorial code is available on GitHub at https://github.com/cornflourblue/angular-14-role-based-authorization-example.

Here it is in action: (See on StackBlitz at https://stackblitz.com/edit/angular-14-role-based-authorization-example)


Run the Angular 14 Role Based Authorization Example Locally

  1. Install Node.js and npm from https://nodejs.org.
  2. Download or clone the tutorial project source code from https://github.com/cornflourblue/angular-14-role-based-authorization-example
  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. Start the application by running npm start from the command line in the project root folder, this will build the application and automatically launch it in the browser on the URL http://localhost:4200.

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.

For more info on setting up your local Angular dev environment see Angular - Setup Development Environment.


Connect the Angular App with a .NET 6.0 API

For full details about the example .NET 6 API see the post .NET 6.0 - Role Based Authorization Tutorial with Example API. 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 project source code from https://github.com/cornflourblue/dotnet-6-role-based-authorization-api
  3. 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.
  4. 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 connected with the .NET API.


Connect the Angular App with a Node.js API

For full details about the example Node.js API see the post Node.js - Role Based Authorization Tutorial with Example API. But to get up and running quickly just follow the below steps.

  1. Install NodeJS and NPM from https://nodejs.org.
  2. Download or clone the project source code from https://github.com/cornflourblue/node-role-based-authorization-api
  3. 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.
  4. 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.js role based auth API.


Angular 14 Project Structure

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 application. For more info about the Angular CLI see https://angular.io/cli.

The app and code structure of the tutorial mostly follows 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 (home, admin & login), other shared/common code such as services, models, helpers etc are placed in folders prefixed with an underscore _ to easily differentiate them and group them together at the top of the folder structure.

Barrel Files

The index.ts files in each folder are barrel files that group the exported modules from a folder together so they can be imported using the folder path rather than the full module path, and to enable importing multiple modules in a single import (e.g. import { AuthenticationService, UserService } from '../_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').

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

 

Auth Guard

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

The auth guard is an angular route guard that's used to prevent unauthenticated or 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 authentication service to check if the user is logged in, if they are logged in it checks if their role is authorized to access the requested route. If they are logged in and authorized the canActivate() method returns true, otherwise it returns false and redirects the user to the login 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 page and admin page routes.

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

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

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

    canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
        const user = this.authenticationService.userValue;
        if (user) {
            // check if route is restricted by role
            const { roles } = route.data;
            if (roles && !roles.includes(user.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(['/login'], { queryParams: { returnUrl: state.url } });
        return false;
    }
}

Back to top

 

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 user is automatically logged out of the application, all other errors are re-thrown up to the calling service to be handled.

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 { AuthenticationService } from '@app/_services';

@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
    constructor(private authenticationService: AuthenticationService) { }

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

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

Back to top

 

Fake Backend Provider

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

In order to run and test the Angular application without a real backend API, the example uses a fake backend that intercepts the HTTP requests from the Angular app and send 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 or this article.

The fake backend provider contains a hardcoded collection of users and provides fake implementations for the api endpoints "authenticate", "get user by id", and "get all users", these would be handled by a real api and database in a production application.

The "authenticate" route is used for logging in to the application and is publicly accessible, the "get user by id" route is restricted to authenticated users in any role, however regular users can only access their own user record while admin users can access any user record. The "get all users" route is restricted to admin users only.

If the request doesn't match any of the faked routes it is passed through as a real HTTP request to the backend API.

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 { Role } from '@app/_models';

const users = [
    { id: 1, username: 'admin', password: 'admin', firstName: 'Admin', lastName: 'User', role: Role.Admin },
    { id: 2, username: 'user', password: 'user', firstName: 'Normal', lastName: 'User', role: Role.User }
];

@Injectable()
export class FakeBackendInterceptor implements HttpInterceptor {
    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        const { url, method, headers, body } = request;

        return handleRoute();

        function handleRoute() {
            switch (true) {
                case url.endsWith('/users/authenticate') && method === 'POST':
                    return authenticate();
                case url.endsWith('/users') && method === 'GET':
                    return getUsers();
                case url.match(/\/users\/\d+$/) && method === 'GET':
                    return getUserById();
                default:
                    // pass through any requests not handled above
                    return next.handle(request);
            }

        }

        // route functions

        function authenticate() {
            const { username, password } = body;
            const user = users.find(x => x.username === username && x.password === password);
            if (!user) return error('Username or password is incorrect');
            return ok({
                id: user.id,
                username: user.username,
                firstName: user.firstName,
                lastName: user.lastName,
                role: user.role,
                token: `fake-jwt-token.${user.id}`
            });
        }

        function getUsers() {
            if (!isAdmin()) return unauthorized();
            return ok(users);
        }

        function getUserById() {
            if (!isLoggedIn()) return unauthorized();

            // only admins can access other user records
            if (!isAdmin() && currentUser()?.id !== idFromUrl()) return unauthorized();

            const user = users.find(x => x.id === idFromUrl());
            return ok(user);
        }

        // helper functions

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

        function unauthorized() {
            return throwError(() => ({ status: 401, error: { message: 'unauthorized' } }))
                .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 error(message: string) {
            return throwError(() => ({ status: 400, error: { message } }))
                .pipe(materialize(), delay(500), dematerialize());
        }

        function isLoggedIn() {
            const authHeader = headers.get('Authorization') || '';
            return authHeader.startsWith('Bearer fake-jwt-token');
        }

        function isAdmin() {
            return currentUser()?.role === Role.Admin;
        }

        function currentUser() {
            if (!isLoggedIn()) return;
            const id = parseInt(headers.get('Authorization')!.split('.')[1]);
            return users.find(x => x.id === id);
        }

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

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

Back to top

 

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 { AuthenticationService } from '@app/_services';

@Injectable()
export class JwtInterceptor implements HttpInterceptor {
    constructor(private authenticationService: AuthenticationService) { }

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

        return next.handle(request);
    }
}

Back to top

 

Role Model

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

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

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

Back to top

 

User Model

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

The user model is a small interface that defines the properties of a user, including their role and jwt auth token? (the trailing question mark ? makes the property optional in TypeScript).

import { Role } from "./role";

export interface User {
    id: number;
    firstName: string;
    lastName: string;
    username: string;
    role: Role;
    token?: string;
}

Back to top

 

Authentication Service

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

The authentication service is used to login & logout of the Angular app, it notifies other components when the user logs in & out, and allows access the currently logged in user.

How RxJS is used by the service

RxJS Subjects and Observables are used to store the current user object and notify other components when the user logs in and out of the app. Angular components can subscribe() to the public user: Observable property to be notified of changes, and notifications are sent when the this.userSubject.next() method is called in the login() and logout() methods, passing the argument to each subscriber.

The RxJS BehaviorSubject is a special type of Subject that keeps hold of the current value and emits it to any new subscribers as soon as they subscribe, while regular Subjects don't store the current value and only emit values that are published after a subscription is created. For more info on component communication with RxJS see Angular 14 - Communicating Between Components with RxJS Observable & Subject.

Auth service methods and properties

The login() method sends the user credentials to the API via an HTTP POST request for authentication. If successful the user object including a JWT auth token are stored in localStorage to keep the user logged in between page refreshes. The user object is then published to all subscribers with the call to this.userSubject.next(user);.

The logout() method removes the current user object from local storage, publishes null to the userSubject to notify all subscribers that the user has logged out, and redirects the user to the login page.

The constructor() of the service initialises the userSubject with the user object from localStorage which enables the user to stay logged in between page refreshes or after the browser is closed. The public user property is then set to this.userSubject.asObservable(); which allows other components to subscribe to the user Observable but doesn't allow them to publish to the userSubject, this is so logging in and out of the app can only be done via the authentication service.

The userValue getter allows other components an easy way to get the value of the currently logged in user without having to subscribe to the user Observable.

NOTE: If you don't like the idea of storing the current user details in local storage, all you need to do is change the 3 references to localStorage in this file. Other options are session storage, cookies, or you could simply not store the user details in the browser, although be aware that with this last option the user will be automatically logged out if they refresh the page.

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

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

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

    constructor(
        private router: Router,
        private http: HttpClient
    ) {
        this.userSubject = new BehaviorSubject(JSON.parse(localStorage.getItem('user')!));
        this.user = this.userSubject.asObservable();
    }

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

    login(username: string, password: string) {
        return this.http.post<any>(`${environment.apiUrl}/users/authenticate`, { username, password })
            .pipe(map(user => {
                // store user details and jwt token in local storage to keep user logged in between page refreshes
                localStorage.setItem('user', JSON.stringify(user));
                this.userSubject.next(user);
                return user;
            }));
    }

    logout() {
        // remove user from local storage to log user out
        localStorage.removeItem('user');
        this.userSubject.next(null);
        this.router.navigate(['/login']);
    }
}

Back to top

 

User Service

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

The user service contains just a couple of methods for retrieving user data from the api, it acts as the interface between the Angular application and the backend api.

I included the user service to demonstrate accessing secure api endpoints with the http authorization header set after logging in to the application, the auth header is set with a JWT token in the JWT Interceptor. The secure endpoints in the example are mocked routes implemented in the fake backend provider.

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

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

@Injectable({ providedIn: 'root' })
export class UserService {
    constructor(private http: HttpClient) { }

    getAll() {
        return this.http.get<User[]>(`${environment.apiUrl}/users`);
    }

    getById(id: number) {
        return this.http.get<User>(`${environment.apiUrl}/users/${id}`);
    }
}

Back to top

 

Admin Component Template

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

The admin component template contains html and angular 14 template syntax for displaying a list of all users retrieved from a secure api endpoint.

<div class="card mt-4">
    <h4 class="card-header">Admin</h4>
    <div class="card-body">
        <p>This page can be accessed <u>only by administrators</u>.</p>
        <p class="mb-1">All users from secure (admin only) api end point:</p>
        <div *ngIf="loading" class="spinner-border spinner-border-sm"></div>
        <ul *ngIf="users">
            <li *ngFor="let user of users">{{user.firstName}} {{user.lastName}}</li>
        </ul>
    </div>
</div>

Back to top

 

Admin Component

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

The admin component calls the user service to get all users from a secure api endpoint and assign them to the local users property which is accessible from the admin component template above.

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

import { User } from '@app/_models';
import { UserService } from '@app/_services';

@Component({ templateUrl: 'admin.component.html' })
export class AdminComponent implements OnInit {
    loading = false;
    users: User[] = [];

    constructor(private userService: UserService) { }

    ngOnInit() {
        this.loading = true;
        this.userService.getAll().pipe(first()).subscribe(users => {
            this.loading = false;
            this.users = users;
        });
    }
}

Back to top

 

Home Component Template

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

The home component template contains html and angular 14 template syntax for displaying a simple welcome message and the current user record that is fetched from a secure api endpoint.

<div class="card mt-4">
    <h4 class="card-header">Home</h4>
    <div class="card-body">
        <p>You're logged in with Angular 14 & JWT!!</p>
        <p>Your role is: <strong>{{user.role}}</strong>.</p>
        <p>This page can be accessed by <u>all authenticated users</u>.</p>
        <p class="mb-1">Current user from secure api end point:</p>
        <div *ngIf="loading" class="spinner-border spinner-border-sm"></div>
        <ul *ngIf="userFromApi">
            <li>{{userFromApi.firstName}} {{userFromApi.lastName}}</li>
        </ul>
    </div>
</div>

Back to top

 

Home Component

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

The home component gets the current user from the authentication service and then gets the current user from the api with the user service. We only really needed to get the user from the authentication service, but I included getting it from the user service as well to demonstrate fetching data from a secure api endpoint.

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

import { User } from '@app/_models';
import { UserService, AuthenticationService } from '@app/_services';

@Component({ templateUrl: 'home.component.html' })
export class HomeComponent {
    loading = false;
    user: User;
    userFromApi?: User;

    constructor(
        private userService: UserService,
        private authenticationService: AuthenticationService
    ) {
        this.user = <User>this.authenticationService.userValue;
    }

    ngOnInit() {
        this.loading = true;
        this.userService.getById(this.user.id).pipe(first()).subscribe(user => {
            this.loading = false;
            this.userFromApi = user;
        });
    }
}

Back to top

 

Login Component Template

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

The login component template contains a login form with username and password fields. It displays validation messages for invalid fields when the submit button is clicked. 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.

<div class="col-md-6 offset-md-3 mt-5">
    <div class="alert alert-info">
        <strong>Normal User</strong> - U: user P: user<br />
        <strong>Administrator</strong> - U: admin P: admin
    </div>
    <div class="card">
        <h4 class="card-header">Angular 14 Role Based Auth Example</h4>
        <div class="card-body">
            <form [formGroup]="loginForm" (ngSubmit)="onSubmit()">
                <div class="mb-3">
                    <label class="form-label">Username</label>
                    <input type="text" formControlName="username" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.username.errors }" />
                    <div *ngIf="submitted && f.username.errors" class="invalid-feedback">
                        <div *ngIf="f.username.errors.required">Username is required</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>
                <button [disabled]="loading" class="btn btn-primary">
                    <span *ngIf="loading" class="spinner-border spinner-border-sm me-1"></span>
                    Login
                </button>
                <div *ngIf="error" class="alert alert-danger mt-3 mb-0">{{error}}</div>
            </form>
        </div>
    </div>
</div>

Back to top

 

Login Component

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

The login component uses the authentication service to login to the application. If the user is already logged in they are automatically redirected to the home page.

The loginForm: FormGroup object defines the form controls and validators, and is used to access data entered into the form. The FormGroup is part of the Angular Reactive Forms module and is bound to the login template above with the formGroup directive ([formGroup]="loginForm").

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 { AuthenticationService } from '@app/_services';

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

    constructor(
        private formBuilder: FormBuilder,
        private route: ActivatedRoute,
        private router: Router,
        private authenticationService: AuthenticationService
    ) {
        // redirect to home if already logged in
        if (this.authenticationService.userValue) {
            this.router.navigate(['/']);
        }
    }

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

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

    onSubmit() {
        this.submitted = true;

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

        this.loading = true;
        this.authenticationService.login(this.f.username.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.error = error;
                    this.loading = false;
                }
            });
    }
}

Back to top

 

App Routing Module

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

Routing for the Angular app is configured as an array of Routes, each component is mapped to a path so the Angular Router knows which component to display based on the URL in the browser address bar. The home and admin routes are secured by passing the AuthGuard to the canActivate property of the route. The admin route also sets the roles data property to [Role.Admin] so only admin users can access it.

The Routes array is passed to the RouterModule.forRoot() method which creates a routing module with all of the app routes configured, and also includes all of the Angular Router providers and directives such as the <router-outlet></router-outlet> directive. For more information on Angular Routing and Navigation see https://angular.io/guide/router.

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

import { HomeComponent } from './home';
import { AdminComponent } from './admin';
import { LoginComponent } from './login';
import { AuthGuard } from './_helpers';
import { Role } from './_models';

const routes: Routes = [
    {
        path: '',
        component: HomeComponent,
        canActivate: [AuthGuard]
    },
    {
        path: 'admin',
        component: AdminComponent,
        canActivate: [AuthGuard],
        data: { roles: [Role.Admin] }
    },
    {
        path: 'login',
        component: LoginComponent
    },

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

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

Back to top

 

App Component Template

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

The app component template is the root component template of the application, it contains the main nav bar which is only displayed for authenticated users, and a router-outlet directive for displaying the contents of each view based on the current route / path.

The nav bar contains links for the home page, the admin page and a logout link. The admin link is only displayed for users in the Admin role by using the isAdmin getter in an *ngIf directive. The logout link calls the logout() method of the app component on click.

<!-- nav -->
<nav class="navbar navbar-expand navbar-dark bg-dark px-3" *ngIf="user">
    <div class="navbar-nav">
        <a class="nav-item nav-link" routerLink="/" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}">Home</a>
        <a class="nav-item nav-link" routerLink="/admin" routerLinkActive="active" *ngIf="isAdmin">Admin</a>
        <button class="btn btn-link nav-item nav-link" (click)="logout()">Logout</button>
    </div>
</nav>

<!-- main app container -->
<div class="container">
    <router-outlet></router-outlet>
</div>

Back to top

 

App Component

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

The app component is the root component of the application, 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 user observable in the authentication service so it can reactively show/hide the main navigation bar 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. This isn't necessary in the root app component because it will only be destroyed when the entire app is closed.

The app component contains a logout() method which is called from the logout link in the main nav bar above to log the user out and redirect them to the login page. The isAdmin() getter returns true if the logged in user is in the Admin role, or false for non-admin users.

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

import { AuthenticationService } from './_services';
import { User, Role } from './_models';

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

    constructor(private authenticationService: AuthenticationService) {
        this.authenticationService.user.subscribe(x => this.user = x);
    }

    get isAdmin() {
        return this.user?.role === Role.Admin;
    }

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

Back to top

 

App Module

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

The app module defines the root module of the application along with metadata about the module. For more info 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 providers located below the comment // provider used to create fake backend.

import { NgModule } 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 { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';

import { JwtInterceptor, ErrorInterceptor } from './_helpers';
import { HomeComponent } from './home';
import { AdminComponent } from './admin';
import { LoginComponent } from './login';

@NgModule({
    imports: [
        BrowserModule,
        ReactiveFormsModule,
        HttpClientModule,
        AppRoutingModule
    ],
    declarations: [
        AppComponent,
        HomeComponent,
        AdminComponent,
        LoginComponent
    ],
    providers: [
        { 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 { }

Back to top

 

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'
};

Back to top

 

Development Environment Config

Path: /src/environments/environment.ts

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

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

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

Back to top

 

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 14 - Role Based Authorization Tutorial with Example</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">

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

Back to top

 

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));

Back to top

 

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 14 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.

Back to top

 

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-14-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": "^14.2.0",
        "@angular/common": "^14.2.0",
        "@angular/compiler": "^14.2.0",
        "@angular/core": "^14.2.0",
        "@angular/forms": "^14.2.0",
        "@angular/platform-browser": "^14.2.0",
        "@angular/platform-browser-dynamic": "^14.2.0",
        "@angular/router": "^14.2.0",
        "rxjs": "~7.5.0",
        "tslib": "^2.3.0",
        "zone.js": "~0.11.4"
    },
    "devDependencies": {
        "@angular-devkit/build-angular": "^14.2.8",
        "@angular/cli": "~14.2.8",
        "@angular/compiler-cli": "^14.2.0",
        "@types/jasmine": "~4.0.0",
        "jasmine-core": "~4.3.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.7.2"
    }
}

Back to top

 

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/config/tsconfig.

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
    }
}

Back to top

 


Subscribe or Follow Me For Updates

Subscribe to my YouTube channel or follow me on Twitter, Facebook or GitHub to be notified when I post new content.

Other than coding...

I'm currently attempting to travel around Australia by motorcycle with my wife Tina on a pair of Royal Enfield Himalayans. You can follow our adventures on YouTube, Instagram and Facebook.


Need Some Angular 14 Help?

Search fiverr to find help quickly from experienced Angular 14 developers.