How-To: ClassMarker Integration in Fabman for Machine Access Control

Use Case

For certain machines in your Fabman-managed workspace, users must pass a test before they can use the machine. In the Fabman Members Portal, the machine’s description includes a link to an online test hosted on ClassMarker. When a user successfully completes the test, Fabman automatically creates a training record for that user, granting them access to the machine.

This guide walks you through integrating ClassMarker with Fabman to enable this feature.


Prerequisites

  1. Fabman Account: Admin access to configure resources and trainings.
  2. ClassMarker Account: Access to create tests and manage webhooks.
  3. Training Records in Fabman: Create training courses that correspond to the required machine skills.
  4. Tests in ClassMarker: Create tests that match your Fabman trainings.

Integration Overview

The integration consists of three main steps:

  1. Setting up the Webhook in ClassMarker:
  • Configure a webhook in ClassMarker to notify Fabman when a test is completed.
  • Map ClassMarker tests to Fabman training records in the webhook configuration.
  1. Adding Training Links in Fabman:
  • Include a link to the ClassMarker test in the resource description on Fabman.
  • Use Fabman’s user-specific ID in the URL to ensure the test results are associated with the correct user.
  1. Source Code Configuration:
  • Modify the provided PHP webhook script with your Fabman API token, ClassMarker test-to-training mapping, and webhook secret.

Step-by-Step Guide

1. Configure the Webhook in ClassMarker

  1. Log in to ClassMarker and navigate to: Tests → Settings → Webhooks.
  2. Create a new webhook:
  • Webhook URL: Provide the URL of your webhook server where the PHP script will be hosted (e.g., https://yourdomain.com/webhooks/classmarker.php).
  • Secret Code: Generate a unique secret code and save it (you’ll need this later).
  • Select the option to Send Webhook on Test Completion.
  1. Modify the PHP Webhook Script:
  • Download the PHP webhook script (scroll down to the end of the article).
  • Update the following configurations:
    • Fabman API Token: Your Fabman API token for authorization.
    • Test-to-Training Mapping: Map ClassMarker test IDs to Fabman training IDs.
    • Webhook Secret: Add the secret code generated in ClassMarker.
  1. Upload the script to your server.

2. Add Training Links in Fabman

  1. Log in to Fabman as an admin and navigate to Resources.
  2. For each resource requiring a test, include the test link in the Notes field:
  • Example link:
https://www.classmarker.com/online-test/start/?quiz=tkr631f0f1c236d7&cm_user_id={userId}
  • Replace tkr631f0f1c236d7 with the specific test ID from ClassMarker.
  • Use {userId} as a placeholder. Fabman automatically replaces it with the user’s ID when they access the link.
  1. Save the changes. Users will now see the test link in the Fabman Members Portal under the resource description.

3. Test the Integration

  1. Navigate to the Fabman Members Portal as a test user.
  2. Open a resource requiring a test and click the provided link to take the ClassMarker test.
  3. Complete the test with a passing score.
  4. Verify that:
  • The test completion triggers the webhook.
  • A corresponding training record is created in Fabman.
  • The user is granted access to the resource.

Final Notes

  • Debugging: Use the log file (ClassMarker.log) to monitor webhook activity and troubleshoot issues.
  • Security: Ensure your webhook server uses HTTPS and restrict access to authorized IPs if possible.
  • Future Enhancements: Extend the script to support additional features, such as email notifications or detailed reporting.

With this integration, your Fabman workspace will automatically grant access to resources based on test results from ClassMarker. If you have any questions, feel free to ask in the comments!


Source Code

Here is the PHP code for the webhook script:

<?php

/**
 * PHP webhook code to link ClassMarker.com to fabman.io
 * Documentation:
 * - ClassMarker Webhooks: https://www.classmarker.com/online-testing/api/webhooks/
 * - Fabman API: https://www.fabman.io
 */

### CONFIGURATION SECTION ###

// Fabman API configuration
$token = "25b077b4-7cgg-46f1-1234-4b985a708ee5"; // Set your Fabman API token here
$APIurl = "https://fabman.io/api/v1/";

// Map ClassMarker test link IDs to Fabman training IDs
$fabman_training_id = array(
    "tkr631f0f1c236d7" => 26,
    "q7t631f12434a32a" => 21,
);

// Log file path for recording webhook events
$log_file = "ClassMarker.log";

// Secret key for verifying ClassMarker webhook payloads
define('CLASSMARKER_WEBHOOK_SECRET', 'gQD45tfiUznOKzQ');

### END CONFIGURATION SECTION ###

/**
 * Centralized logging function.
 * @param string $message The message to log.
 * @param string $level The log level (e.g., INFO, ERROR, DEBUG).
 */
function log_message($message, $level = "INFO")
{
    global $log_file;
    $timestamp = date("Y-m-d H:i:s");
    $log_entry = "[$timestamp] [$level] $message\n";

    // Write log entry to file
    file_put_contents($log_file, $log_entry, FILE_APPEND);
}

// Verify the authenticity of the incoming ClassMarker webhook request
function verify_classmarker_webhook($json_data, $header_hmac_signature)
{
    $calculated_signature = base64_encode(hash_hmac('sha256', $json_data, CLASSMARKER_WEBHOOK_SECRET, true));
    return ($header_hmac_signature === $calculated_signature);
}

// Retrieve the HMAC signature from ClassMarker webhook header
$header_hmac_signature = $_SERVER['HTTP_X_CLASSMARKER_HMAC_SHA256'] ?? null;

// Get the JSON payload from the request body
$json_string_payload = file_get_contents('php://input');

// Verify the payload using the secret
$verified = verify_classmarker_webhook($json_string_payload, $header_hmac_signature);

// Decode the JSON payload into an associative array
$array_payload = json_decode($json_string_payload);

if ($verified) {
    // Respond with HTTP 200 to acknowledge receipt of the webhook
    http_response_code(200);

    // Extract member and training information
    $member = $array_payload->result->cm_user_id;
    $training = $fabman_training_id[$array_payload->link->link_url_id] ?? null;

    if (!$training) {
        log_message("Training ID not found for member $member.", "ERROR");
        exit;
    }

    log_message("Webhook received: member $member, training $training");

    // Check if the user passed the test
    if ($array_payload->result->passed === 'true') {
        log_message("Member $member passed the training $training.", "INFO");

        // Collect additional details for the training record
        $view_results_url = $array_payload->result->view_results_url;
        $certificate_url = $array_payload->result->certificate_url;
        $percentage = $array_payload->result->percentage;
        $percentage_passmark = $array_payload->result->percentage_passmark;
        $notes = "Permission granted automatically after online test.<br><br>"
               . "Test result: $percentage% (Minimum required: $percentage_passmark%)<br>"
               . "View results: $view_results_url<br>"
               . "Download certificate: $certificate_url";

        // Attempt to add the training record in Fabman
        if (add_training($member, date("Y-m-d"), $training, $notes)) {
            log_message("Training added successfully for member $member.", "INFO");
        } else {
            log_message("Failed to record training for member $member (on-site training required).", "WARNING");
        }
    } else {
        log_message("Member $member failed the training $training.", "INFO");
    }
} else {
    // Respond with HTTP 400 for an invalid request
    http_response_code(400);
    log_message("Webhook verification failed. Invalid request.", "ERROR");
}

/**
 * Add a training record in Fabman.
 * @param string $memberId The ID of the member in Fabman.
 * @param string $date The date of the training.
 * @param int $trainingCourse The ID of the training course in Fabman.
 * @param string $notes Optional notes for the training record.
 * @return bool True if the training record is added successfully, false otherwise.
 */
function add_training($memberId, $date, $trainingCourse, $notes = "")
{
    $data = array("date" => $date, "trainingCourse" => $trainingCourse, "notes" => $notes);
    $result = callAPI("POST", "members/" . $memberId . "/trainings", json_encode($data));

    // Check if the API call was successful
    return $result['http_code'] === 201;
}

/**
 * Make an API call to Fabman.
 * @param string $method The HTTP method (GET, POST, PUT, DELETE).
 * @param string $url The endpoint URL (relative to the API base URL).
 * @param mixed $data Optional data for POST/PUT requests.
 * @return array An associative array containing the HTTP code, data, and response headers.
 */
function callAPI($method, $url, $data = false)
{
    global $token, $APIurl;

    $url = $APIurl . $url;

    // Initialize cURL
    $curl = curl_init();
    $header = array('Authorization: Bearer ' . $token);

    // Configure the cURL request based on the method
    switch ($method) {
        case "POST":
            curl_setopt($curl, CURLOPT_POST, 1);
            if ($data) {
                curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
                array_push($header, 'Content-Type: application/json', 'Content-Length: ' . strlen($data));
            }
            break;

        case "PUT":
            curl_setopt($curl, CURLOPT_CUSTOMREQUEST, "PUT");
            if ($data) {
                curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
                array_push($header, 'Content-Type: application/json', 'Content-Length: ' . strlen($data));
            }
            break;

        case "DELETE":
            curl_setopt($curl, CURLOPT_CUSTOMREQUEST, "DELETE");
            break;

        default:
            if ($data) {
                $url = sprintf("%s?%s", $url, http_build_query($data));
            }
    }

    // Set headers and options for cURL
    curl_setopt($curl, CURLOPT_HTTPHEADER, $header);
    curl_setopt($curl, CURLOPT_HEADER, true);
    curl_setopt($curl, CURLOPT_URL, $url);
    curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);

    // Execute the cURL request
    $result = curl_exec($curl);

    // Extract the response header and body
    $header_size = curl_getinfo($curl, CURLINFO_HEADER_SIZE);
    $header = substr($result, 0, $header_size);
    $body = substr($result, $header_size);

    // Parse response headers
    $response_headers = [];
    $lines = explode("\r\n", $header);
    foreach ($lines as $line) {
        if (strpos($line, ':') !== false) {
            list($key, $value) = explode(': ', $line, 2);
            $response_headers[$key] = $value;
        }
    }

    $http_code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
    curl_close($curl);

    return array(
        "http_code" => $http_code,
        "data" => json_decode($body),
        "header" => $response_headers
    );
}

?>
1 Like

This integration would be really cool with the following topic:

TLDR: To require two trainings before having access to the booking. Like a yearly security exam + a basic laser training.

We did something similar with POC bridge. Thanks @roland for help during the development.
Here´s the repo:

2 Likes