Skip to content

Commit

Permalink
Merge pull request #88 from AndrewPoppe/83-api-methods
Browse files Browse the repository at this point in the history
Add API Methods - address #83
  • Loading branch information
AndrewPoppe committed Oct 9, 2023
2 parents f2a8731 + d8e73ef commit 60ad2d6
Show file tree
Hide file tree
Showing 10 changed files with 675 additions and 18 deletions.
78 changes: 62 additions & 16 deletions README.md

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions REDCapPRO.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
use ExternalModules\AbstractExternalModule;
use ExternalModules\Framework;

require_once "src/classes/APIHandler.php";
require_once "src/classes/APIParticipantEnroll.php";
require_once "src/classes/APIParticipantRegister.php";
require_once "src/classes/AjaxHandler.php";
require_once "src/classes/Auth.php";
require_once "src/classes/CsvEnrollImport.php";
Expand Down Expand Up @@ -1122,4 +1125,13 @@ public function includeFont()
'<link href="https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible:ital,wght@0,400;0,700;1,400;1,700&display=swap" rel="stylesheet">' .
'<style> body, a, a:visited, a.nav-link { font-family: "Atkinson Hyperlegible", sans-serif !important; } </style>';
}

public function getProjectlessUrl(string $path, bool $noAuth, bool $useApiEndpoint)
{
$pid = $_GET['pid'];
unset($_GET['pid']);
$result = $this->framework->getUrl($path, true, true);
$_GET['pid'] = $pid;
return $result;
}
}
21 changes: 20 additions & 1 deletion config.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,21 @@
"key": "mfa",
"name": "<strong>Multi-Factor Authentication</strong>:<br>Should participants be required to use multi-factor authentication (MFA) when logging in? If so, they will be required to enter a code sent to their email address after entering their username and password. This is an additional security measure to prevent unauthorized access.<br><em>Note: this setting enables the MFA option globally, but it still must be enabled in the project settings to take effect</em>",
"type": "checkbox"
},
{
"key": "api-descriptive",
"name": "<div class='p-2' style='background-image: linear-gradient(rgb(255, 248, 197), rgb(255, 248, 197)); border: 1px solid rgba(168, 133, 37, 0.4); border-radius: 0.375rem;'><strong>API Settings</strong><p>These settings control the use of the API. Please take care to understand how the API may be used before enabling it in the system.</p> </div>",
"type": "descriptive"
},
{
"key": "api-enabled-system",
"name": "<div class='p-2' style='background-image: linear-gradient(rgb(255, 248, 197), rgb(255, 248, 197)); border: 1px solid rgba(168, 133, 37, 0.4); border-radius: 0.375rem;'><strong>Enable the API</strong></div>Should the API be enabled for use?<br><em>Note: this setting enables the API option globally, but it still must be enabled in the project settings to take effect</em>",
"type": "checkbox"
},
{
"key": "api-require-admin",
"name": "<div class='p-2' style='background-image: linear-gradient(rgb(255, 248, 197), rgb(255, 248, 197)); border: 1px solid rgba(168, 133, 37, 0.4); border-radius: 0.375rem;'><strong>Restrict API project settings to REDCap administrators</strong></div>Should only REDCap administrators be able to enable/disable the API in project settings? If not, any REDCapPRO manager in the project will be able to enable/disable the API in project settings.<br><em>Note: this setting only applies if the API is enabled globally</em>",
"type": "checkbox"
}
],
"links": {
Expand Down Expand Up @@ -84,7 +99,11 @@
"src/create-password",
"src/forgot-password",
"src/forgot-username",
"src/session_check"
"src/session_check",
"src/api"
],
"no-csrf-pages": [
"src/api"
],
"auth-ajax-actions": [
"exportLogs",
Expand Down
56 changes: 56 additions & 0 deletions src/api.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

namespace YaleREDCap\REDCapPRO;

/** @var REDCapPRO $module */

// This is an API endpoint that can be used to register and enroll participants
// It is NOAUTH and No CSRF, so API token is required
try {

$action = filter_input(INPUT_POST, 'action', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
if ( $action == "register" ) {
$apiHandler = new APIParticipantRegister($module, $_POST);
} elseif ( $action == "enroll" ) {
$apiHandler = new APIParticipantEnroll($module, $_POST);
} else {
throw new \Error("Invalid API action");
}

$projectSettings = new ProjectSettings($module);
if ( !$projectSettings->apiEnabled($apiHandler->project->getProjectId()) ) {
throw new \Error("API is not enabled for this project");
}

if ( !$apiHandler->valid ) {
echo json_encode($apiHandler->errorMessages, JSON_PRETTY_PRINT);
throw new \Error("Invalid API payload");
}
} catch ( \Throwable $e ) {
$module->logError("Error using API", $e);
echo json_encode([
"error" => $e->getMessage(),
], JSON_PRETTY_PRINT);
return;
}

// Only allow Normal Users and above to use the API
if ( ((int) $apiHandler->getRole()) < 2 ) {
echo json_encode([
"error" => "You do not have permission to use the REDCapPRO API",
], JSON_PRETTY_PRINT);
return;
}

// Try to enroll/register the participants
try {
$result = $apiHandler->takeAction();
} catch ( \Throwable $e ) {
$module->logError("Error using the REDCapPRO API", $e);
echo json_encode([
"error" => $e->getMessage(),
], JSON_PRETTY_PRINT);
return;
}

echo json_encode($result, JSON_PRETTY_PRINT);
120 changes: 120 additions & 0 deletions src/classes/APIHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
<?php
namespace YaleREDCap\REDCapPRO;

use \ExternalModules\User;

class APIHandler
{
public $token;
public REDCapPRO $module;
private array $payload;
public User $user;
public array $rights = [];
public \ExternalModules\Project $project;
private array $data = [];
public array $actionData = [];

public function __construct(REDCapPRO $module, array $payload)
{
define("API", true);

$this->module = $module;
$this->payload = $payload;
$this->data = $this->parsePayload();
$this->token = $this->module->framework->sanitizeAPIToken($this->data['token']) ?? 'X';
$this->rights = $this->extractRights();
$this->user = $this->extractUser();
$this->project = $this->extractProject();
$this->actionData = $this->extractActionData();

define('USERID', $this->user->getUsername());
define('PROJECT_ID', $this->rights['project_id']);
$_GET['pid'] = $this->rights['project_id'];
$GLOBALS['Proj'] = new \Project($this->rights['project_id']);
}

public function extractUser()
{
$username = $this->rights['username'];
if ( empty($username) ) {
throw new \Error("Invalid API token");
}
return $this->module->framework->getUser($username);
}

public function extractProject()
{
$project_id = $this->rights['project_id'];
if ( empty($project_id) ) {
throw new \Error("Invalid API token");
}
return $this->module->framework->getProject($project_id);
}

public function extractRights()
{
$rights = $this->getUserRightsFromToken();
if ( empty($rights) ) {
throw new \Error('Invalid API token');
}
return $rights;
}

public function extractActionData()
{
$actionDataString = $this->data['data'];
try {
$actionData = json_decode($actionDataString, true);
} catch ( \Throwable $e ) {
$this->module->logError('Error decoding action data', $e);
}
if ( empty($actionData) ) {
throw new \Error('No import data provided');
}
return $this->module->framework->escape($actionData);
}

private function getUserRightsFromToken() : array
{
$sql = "SELECT * FROM redcap_user_rights WHERE api_token = ?";
$rights = [];
try {
$result = $this->module->framework->query($sql, [ $this->token ]);
$rights = $result->fetch_assoc() ?? [];
} catch ( \Throwable $e ) {
$this->module->logError('Error getting user rights from API token', $e);
} finally {
return $this->module->framework->escape($rights);
}
}

private function parsePayload()
{
try {
$actionDataString = $this->payload['data'] ?? '{}'; // This will be sanitized later
$this->data = $this->module->framework->escape($this->payload);
$this->data['data'] = $actionDataString;
} catch ( \Throwable $e ) {
$this->module->logError('Error parsing payload', $e);
} finally {
return $this->data;
}
}

public function getRole()
{
return $this->module->getUserRole($this->user->getUsername());
}

public function getApiData()
{
return [
'token' => $this->token,
'rights' => $this->rights,
'user' => $this->user->getUsername(),
'project' => $this->project->getProjectId(),
'role' => $this->module->getUserRole($this->user->getUsername()),
'actionData' => $this->actionData
];
}
}
Loading

0 comments on commit 60ad2d6

Please sign in to comment.