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