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.
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!
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.
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!
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',})exportclassAuthGuardimplementsCanActivate {// Injecting AuthService to check authentication status and Router to navigateconstructor(private authService:AuthService,private router:Router) {}// The canActivate method is called by Angular to determine if a route can be activatedcanActivate( route:ActivatedRouteSnapshot, state:RouterStateSnapshot ):boolean {// Perform additional authentication checks if neededthis.authService.checkAuthStatus();// If the user is authenticated, allow the route activationif (this.authService.authenticated) {returntrue; } else {// If the user is not authenticated, redirect to the login page with the return URL// and deny the route activationthis.router.navigate(['login'], { queryParams: { returnUrl:state.url }, });returnfalse; } }}
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',})exportclassDbService {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. */publicsave(key:string, value:string):void {try {// Attempt to save the key-value pair to the local storagelocalStorage.setItem(key, value); } catch (error) {// Log an error message if the operation failsconsole.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. */publicload(key:string):string|null {try {// Attempt to retrieve the value associated with the specified key from the local storagereturnlocalStorage.getItem(key); } catch (error) {// Log an error message if the operation failsconsole.error(`Failed to load value for key "${key}" from local storage.`, error );returnnull; } }/** * Method to remove a key-value pair from the local storage by its key. * @param key - The key of the value to be removed. */publicremove(key:string):void {try {// Attempt to remove the key-value pair associated with the specified key from the local storagelocalStorage.removeItem(key); } catch (error) {// Log an error message if the operation failsconsole.error(`Failed to remove value for key "${key}" from local storage.`, error ); } }/** * Method to clear all key-value pairs from the local storage. */publicclear():void {try {// Attempt to clear all key-value pairs from the local storagelocalStorage.clear(); } catch (error) {// Log an error message if the operation failsconsole.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',})exportclassAuthService {public authenticated:any;constructor(private http:HttpClient,private router:Router,private db:DbService ) {}// Method to check the user's authentication statuscheckAuthStatus() {// Retrieve the access token from local storageconsttoken=this.db.load('access_token');// Check if the token existsif (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 truethis.authenticated =true; } else {// If the token does not exist or is invalid, set the authenticated status to falsethis.authenticated =false; } }// Method to initiate the OAuth login processlogin() {// Define the OAuth parametersconstparams= ['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 endpointwindow.location.href =environment.oauthAuthorizeUrl +'?'+params.join('&'); }// Method to log out the userlogout() {// Clear authentication status and local storagethis.authenticated =false;this.db.clear();// Redirect the user to the login pagethis.router.navigateByUrl('login'); }// Method to get the access token using the authorization codegetAccessToken(code:string) {// Define the payload for the OAuth token requestconstpayload=newHttpParams().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 endpointthis.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 responseif ((response asany)['access_token']) {// Save the access token and mark the user as authenticatedthis.db.save('access_token', (response asany)['access_token']);this.authenticated =true;this.router.navigateByUrl('tasks'); } else {// If the access token is not present, mark the user as unauthenticatedthis.authenticated =false;this.router.navigateByUrl('login'); } }, (error) => {// Handle any errors that occur during the requestconsole.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.
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.