Part 2: Services & Dependencies

Overview

To achieve our application's desired functionality, we'll create two services. These services will manage the access token storage and facilitate communication with the ProcessMaker API via the SDK.

Completed code: live example / download example

Step 1: Create the OAuth Client App

Follow this step-by-step guide to create your OAuth application here.

You MUST create the OAuth Client Application in order to consume the ProcessMaker API.

Step 2: Set Up

Main TS

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

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

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

In the above file, we are starting Angular. Note line 5: this is where we bring in our environment variables which we will use to contain all our sensitive information.

The .gitignore file already contains the rule to exclude your actual environment.ts file from any commits. Just make sure to copy and rename the example file!

The Index

index.html
<html lang="en">
	<head>
		<meta http-equiv="X-UA-Compatible" content="IE=edge" />
		<meta charset="utf-8" />
		<meta
			name="viewport"
			content="width=device-width, initial-scale=1, shrink-to-fit=no" />
		<link
			rel="stylesheet"
			href="https://cdn.jsdelivr.net/npm/bootstrap@4.1.3/dist/css/bootstrap.min.css"
			integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO"
			crossorigin="anonymous" />
		<link
			rel="stylesheet"
			href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.2/font/bootstrap-icons.css" />
		<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.1.3/dist/js/bootstrap.bundle.min.js"></script>

		<script
			src="https://code.jquery.com/jquery-3.3.1.slim.min.js"
			integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo"
			crossorigin="anonymous"></script>
		<script
			src="https://cdn.jsdelivr.net/npm/popper.js@1.14.3/dist/umd/popper.min.js"
			integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49"
			crossorigin="anonymous"></script>
		<script
			src="https://cdn.jsdelivr.net/npm/bootstrap@4.1.3/dist/js/bootstrap.min.js"
			integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy"
			crossorigin="anonymous"></script>
		<title>ProcessMaker 4 - Angular</title>
		<base href="/" />
		<meta name="viewport" content="width=device-width, initial-scale=1" />
		<link rel="icon" type="image/x-icon" href="favicon.ico" />
	</head>
	<body>
		<app-root></app-root>
	</body>
</html>

We are utilizing getbootstrap.com, as that is what the ProcessMaker form builder is built with. Our forms utilize the 12 column grid system that bootstrap is so well known for, as well as other niceties that make our lives easier.

There are CDNs in the html file for fewer steps during installation, but if you prefer to have a local offline (if you are behind a corporate VPN perhaps), you can replace the CDN links.

Note: The current version of the application uses bootstrap version 4.1.3 and jquery 3.3.1.

AppModule

Next, we'll update the app.module.ts file from the previous section to incorporate the Angular router. This is going to tell the application what to render when a user does an action, such as opening the form.

This is the modified code.

app.module.ts
import { NgModule } from '@angular/core';
import '@angular/compiler';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';
import { AppComponent } from './components/app.component';
import { AppRoutingModule } from './routing/app-routing.module';

@NgModule({
	declarations: [AppComponent],
	imports: [
		BrowserModule,
		HttpClientModule,
		FormsModule,
		ReactiveFormsModule,
		CommonModule,
		AppRoutingModule,
	],
	providers: [],
	bootstrap: [AppComponent],
})
export class AppModule {}

Router

Create the directory routing inside src/app/, and then add the file app-routing.module.ts. For now, we will just make it a very simple file since it will not be used yet, at least not until we have somewhere to route TO!

app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

const routes: Routes = [
	{
		path: '',
		redirectTo: 'tasks',
		pathMatch: 'full',
		runGuardsAndResolvers: 'always',
	},
	{
		path: '**',
		redirectTo: 'tasks',
		runGuardsAndResolvers: 'always',
	},
];

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

Right now, you will still see the Hello John on the webpage. We need to add a few more things before we will start seeing our application come together.

Guard

The Auth Guard is akin to a security checkpoint. Before navigating to a route (think of it as a destination within your application), this checkpoint checks if the user has the necessary permissions to access that route.

In more technical terms:

  • It's a service that implements a specific interface (CanActivate, for instance).

  • When a user tries to navigate to a route, the Auth Guard runs its logic.

  • If the logic returns true, navigation proceeds.

  • If it returns false, navigation is halted, often redirecting the user elsewhere.

For example, if you have a route that should only be accessible to logged on users, as is our case, an Auth Guard can check if the user is logged on. If they are, they can proceed to the inbox or form. If not, they will be redirected to the log on page.

Here is the code for our guard. Place it inside a new folder called guards and name is auth.guard.ts.

auth.guard.ts
import { Injectable } from '@angular/core';
import {
	ActivatedRouteSnapshot,
	CanActivate,
	Router,
	RouterStateSnapshot,
} from '@angular/router';
import { AuthService } from '../services/auth.service';

@Injectable({
	providedIn: 'root',
})
export class AuthGuard implements CanActivate {
	// Injecting AuthService to check authentication status and Router to navigate
	constructor(private authService: AuthService, private router: Router) {}

	// The canActivate method is called by Angular to determine if a route can be activated
	canActivate(
		route: ActivatedRouteSnapshot,
		state: RouterStateSnapshot
	): boolean {
		// Perform additional authentication checks if needed
		this.authService.checkAuthStatus();

		// If the user is authenticated, allow the route activation
		if (this.authService.authenticated) {
			return true;
		} else {
			// If the user is not authenticated, redirect to the login page with the return URL
			// and deny the route activation
			this.router.navigate(['login'], {
				queryParams: { returnUrl: state.url },
			});
			return false;
		}
	}
}

Step 3: Services

DB

This file interfaces with local storage for basic CRUD operations like saving the access token.

db.service.ts
import { Injectable } from '@angular/core';

@Injectable({
	providedIn: 'root',
})
export class DbService {
	constructor() {}

	/**
	 * Method to save a key-value pair to the local storage.
	 * @param key - The key under which the value will be stored.
	 * @param value - The value to be stored.
	 */
	public save(key: string, value: string): void {
		try {
			// Attempt to save the key-value pair to the local storage
			localStorage.setItem(key, value);
		} catch (error) {
			// Log an error message if the operation fails
			console.error(
				`Failed to save value for key "${key}" to local storage.`,
				error
			);
		}
	}

	/**
	 * Method to load a value from the local storage by its key.
	 * @param key - The key of the value to be retrieved.
	 * @returns The value associated with the specified key, or null if the key does not exist.
	 */
	public load(key: string): string | null {
		try {
			// Attempt to retrieve the value associated with the specified key from the local storage
			return localStorage.getItem(key);
		} catch (error) {
			// Log an error message if the operation fails
			console.error(
				`Failed to load value for key "${key}" from local storage.`,
				error
			);
			return null;
		}
	}

	/**
	 * Method to remove a key-value pair from the local storage by its key.
	 * @param key - The key of the value to be removed.
	 */
	public remove(key: string): void {
		try {
			// Attempt to remove the key-value pair associated with the specified key from the local storage
			localStorage.removeItem(key);
		} catch (error) {
			// Log an error message if the operation fails
			console.error(
				`Failed to remove value for key "${key}" from local storage.`,
				error
			);
		}
	}

	/**
	 * Method to clear all key-value pairs from the local storage.
	 */
	public clear(): void {
		try {
			// Attempt to clear all key-value pairs from the local storage
			localStorage.clear();
		} catch (error) {
			// Log an error message if the operation fails
			console.error('Failed to clear local storage.', error);
		}
	}
}

Auth

This file depends on the db service, as it needs it to store the access token.

There are four methods to this service:

  • login - This is the entry point for when the user will click on the login button.

  • logout - This erases the storage and the access token.

  • checkAuthStatus - This tells us if the user has an access token and is authenticated or not.

  • getAccessToken - This is the main method of the class. This is where we get our access token from ProcessMaker, and you can see we are using here the environment.ts variables.

auth.service.ts
import { HttpClient, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { DbService } from 'src/app/services/db.service';
import { environment } from 'src/environments/environment';

@Injectable({
	providedIn: 'root',
})
export class AuthService {
	public authenticated: any;

	constructor(
		private http: HttpClient,
		private router: Router,
		private db: DbService
	) {}

	// Method to check the user's authentication status
	checkAuthStatus() {
		// Retrieve the access token from local storage
		const token = this.db.load('access_token');

		// Check if the token exists
		if (token) {
			// Optionally, you can add more logic here to validate the token, such as checking its expiration date
			// If the token is valid, set the authenticated status to true
			this.authenticated = true;
		} else {
			// If the token does not exist or is invalid, set the authenticated status to false
			this.authenticated = false;
		}
	}

	// Method to initiate the OAuth login process
	login() {
		// Define the OAuth parameters
		const params = [
			'response_type=code',
			`client_id=${environment.clientId}`, // Use environment variable
			'scope=*',
			encodeURIComponent(`redirect_uri=${environment.redirectUri}`), // Use environment variable
		];
		// Redirect the user to the OAuth authorization endpoint
		window.location.href =
			environment.oauthAuthorizeUrl + '?' + params.join('&');
	}

	// Method to log out the user
	logout() {
		// Clear authentication status and local storage
		this.authenticated = false;
		this.db.clear();
		// Redirect the user to the login page
		this.router.navigateByUrl('login');
	}

	// Method to get the access token using the authorization code
	getAccessToken(code: string) {
		// Define the payload for the OAuth token request
		const payload = new HttpParams()
			.append('grant_type', 'authorization_code')
			.append('code', code)
			.append('client_secret', environment.clientSecret) // Use environment variable
			.append('client_id', environment.clientId); // Use environment variable

		// Make a POST request to the OAuth token endpoint
		this.http
			.post(
				environment.oauthUrl, // Use environment variable
				payload,
				{
					headers: {
						'Content-Type': 'application/x-www-form-urlencoded',
					},
				}
			)
			.subscribe(
				(response) => {
					// Check if the access token is present in the response
					if ((response as any)['access_token']) {
						// Save the access token and mark the user as authenticated
						this.db.save('access_token', (response as any)['access_token']);
						this.authenticated = true;
						this.router.navigateByUrl('tasks');
					} else {
						// If the access token is not present, mark the user as unauthenticated
						this.authenticated = false;
						this.router.navigateByUrl('login');
					}
				},
				(error) => {
					// Handle any errors that occur during the request
					console.error(
						'An error occurred while fetching the access token:',
						error
					);
					this.authenticated = false;
					this.router.navigateByUrl('login');
				}
			);
	}
}

Step 4: The ProcessMaker SDK

We will be utilizing the ProcessMaker SDK for Angular. ProcessMaker utilizes Swagger / OpenAPI for our documentation and API SDKs. Any of the supported SDKs are available to generate via the Artisan CLI tool.

If you want to learn how to use the swagger-based SDK, you can see a video tutorial here:

Below are the list of SDKs that can be generated via the command from within your root directory of your ProcessMaker application.

php artisan processmaker:sdk <sdk-name>
Available SDKs
ada
ada-server
android
apache2
apex
asciidoc
aspnetcore
avro-schema
bash
crystal
c
clojure
cwiki
cpp-qt5-client
cpp-qt5-qhttpengine-server
cpp-pistache-server
cpp-restbed-server
cpp-restsdk
cpp-tizen
cpp-ue4
csharp
csharp-netcore
csharp-nancyfx
dart
dart-dio
dart-dio-next
dart-jaguar
eiffel
elixir
elm
erlang-client
erlang-proper
erlang-server
fsharp-functions
fsharp-giraffe-server
go
go-server
go-gin-server
graphql-schema
graphql-nodejs-express-server
groovy
kotlin
kotlin-server
kotlin-spring
kotlin-vertx
ktorm-schema
haskell-http-client
haskell
java
jaxrs-cxf-client
java-inflector
java-msf4j
java-pkmst
java-play-framework
java-undertow-server
java-vertx-web
jaxrs-cxf
jaxrs-cxf-extended
jaxrs-cxf-cdi
jaxrs-jersey
jaxrs-resteasy
jaxrs-resteasy-eap
jaxrs-spec
javascript
javascript-apollo
javascript-flowtyped
javascript-closure-angular
jmeter
k6
lua
markdown
mysql-schema
nim
nodejs-express-server
objc
ocaml
openapi
openapi-yaml
plantuml
perl
php
php-laravel
php-lumen
php-slim4
php-symfony
php-mezzio-ph
powershell
protobuf-schema
python-legacy
python
python-flask
python-aiohttp
python-blueplanet
r
ruby
ruby-on-rails
ruby-sinatra
rust
rust-server
scalatra
scala-akka
scala-akka-http-server
scala-finch
scala-gatling
scala-lagom-server
scala-play-server
scala-sttp
scalaz
spring
dynamic-html
html
html2
swift5
typescript
typescript-angular
typescript-aurelia
typescript-axios
typescript-fetch
typescript-inversify
typescript-jquery
typescript-nestjs
typescript-node
typescript-redux-query
typescript-rxjs
go-echo-server

For convenience, the application already contains the SDK files that are necessary for the applications functions in the folder api which resides at the root of the application, sibling to the src directory.

Next Steps

Last updated

Logo

© 2024 ProcessMaker, Inc. All Rights Reserved. Except as otherwise permitted by ProcessMaker, this publication, or parts thereof, may not be reproduced in any form, by any method, for any purpose.