Files
itflow/api/v1/validate_api_key.php
johnnyq 2a5533ea2b Roles stage 3: roles and agents in the API and Admin > API Keys rebuilt; new api/v1/roles/read.php where an admin key reads every role with its level per module and agent count, archived on request, and any other key reads only its own user's role, and new api/v1/technicians/read.php listing agents with their role under Support at Read; API role refusals now say what the call needs and what the key's user has, naming the module, level and role, and technicians/time.php needs Read instead of Modify; Admin > API Keys shows each key's agent and role, what it can open and for which clients, whether it works (works until, expires in N days, expired, or won't work with the reason when its agent is disabled, archived, has no role or is missing) and when it was last used, with search by key or agent name; New and Edit windows show the picked agent's access as they are picked and offer only active agents, Create before ticking the copied box opens the Key tab, new keys default to a year and must end after today, and an expired key can be renamed and stay expired; handlers moved to functions/api_keys.php with validation, escaped messages and audit entries naming the agent and what an edit changed, fixing empty names on revoke, delete and bulk delete, a backslash shown for apostrophes in names, the bulk count of keys ticked rather than deleted, keys created already expired and expired keys that could not be renamed; database 3.1.7 adds api_keys.api_key_last_used_at, set at most once a minute in the configured timezone; changelog updated
2026-10-17 21:10:48 -04:00

132 lines
4.3 KiB
PHP

<?php
/*
* API - validate_api_key.php
* Called by API endpoint to validate API key is valid
* Allows execution to continue or exits returning errors to the user
*/
// Includes
require_once __DIR__ . '../../../functions.php';
require_once __DIR__ . "../../../config.php";
// JSON header
header('Content-Type: application/json');
// POST data
$_POST = json_decode(file_get_contents('php://input'), true);
// Get IP & UA
$ip = escapeSql(getIP());
$user_agent = escapeSql($_SERVER['HTTP_USER_AGENT']);
// Temp Added this to work with the new logAction function
$session_ip = $ip;
$session_user_agent = $user_agent;
// Setup return array
$return_arr = array();
// Unauthorised wording
DEFINE("WORDING_UNAUTHORIZED", "HTTP/1.1 401 Unauthorized");
/*
* API Notes:
*
* To avoid over-complicating the app by using PUT and DELETE methods, only going to allow the use of GET and POST methods.
* GET - Retrieving (READ) data
* POST - Inserting (CREATE), Updating (UPDATE) or Deleting (DELETE) data
*
* Data returned as json encoded $return_arr:-
* Success - True/False
* Message - Brief info about a request / failure
* Count - Count of rows affected/returned
* Data - Requested data
*
*/
// Decline methods other than GET/POST
if ($_SERVER['REQUEST_METHOD'] !== "GET" && $_SERVER['REQUEST_METHOD'] !== "POST") {
header("HTTP/1.1 405 Method Not Allowed");
var_dump($_SERVER['REQUEST_METHOD']);
exit();
}
// Check API key is provided
if (!isset($_GET['api_key']) && !isset($_POST['api_key'])) {
header(WORDING_UNAUTHORIZED);
exit();
}
// Set API key variable
if (isset($_GET['api_key'])) {
$api_key = escapeSql($_GET['api_key']);
}
if (isset($_POST['api_key'])) {
$api_key = escapeSql($_POST['api_key']);
}
// Validate API key
if (isset($api_key)) {
$api_key = escapeSql($api_key);
$sql = mysqli_query($mysqli, "SELECT api_key_id, api_key_decrypt_hash, api_key_name, api_key_user_id FROM api_keys WHERE api_key_secret = '$api_key' AND api_key_expire > NOW() LIMIT 1");
// Failed
if (mysqli_num_rows($sql) !== 1) {
// Invalid Key
$url_path = escapeSql(parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH));
mysqli_query($mysqli, "INSERT INTO logs SET log_type = 'API', log_action = 'Failed', log_description = 'Incorrect or expired key (endpoint: $url_path)', log_ip = '$ip', log_user_agent = '$user_agent'");
$return_arr['success'] = "False";
$return_arr['message'] = "Authentication failed. API key is invalid or has expired.";
header(WORDING_UNAUTHORIZED);
echo json_encode($return_arr);
exit();
} else {
// SUCCESS
// Set client ID, company ID & key name
$row = mysqli_fetch_assoc($sql);
$api_key_name = htmlentities($row['api_key_name']);
$api_key_decrypt_hash = $row['api_key_decrypt_hash']; // No sanitization
$api_key_user_id = intval($row['api_key_user_id']);
// When it was last used, for Admin > API Keys - at most once a minute, not every call. In
// ITFlow's own timezone, as the pages show it: the API doesn't set the clocks to it.
$api_tz = mysqli_fetch_row(mysqli_query($mysqli, "SELECT config_timezone FROM settings WHERE company_id = 1"))[0] ?? '';
try {
$api_now = (new DateTime('now', new DateTimeZone($api_tz ?: 'UTC')))->format('Y-m-d H:i:s');
} catch (Exception $e) {
$api_now = date('Y-m-d H:i:s');
}
mysqli_query($mysqli, "UPDATE api_keys SET api_key_last_used_at = '$api_now' WHERE api_key_id = " . intval($row['api_key_id']) . "
AND (api_key_last_used_at IS NULL OR api_key_last_used_at < '$api_now' - INTERVAL 1 MINUTE)");
// Set limit & offset for queries
if (isset($_GET['limit'])) {
$limit = intval($_GET['limit']);
} elseif (isset($_POST['limit'])) {
$limit = intval($_POST['limit']);
} else {
$limit = 50;
}
if (isset($_GET['offset'])) {
$offset = intval($_GET['offset']);
} elseif (isset($_POST['offset'])) {
$offset = intval($_POST['offset']);
} else {
$offset = 0;
}
// When the key is tied to a user, enforce that user's RBAC (module + operation + client scope)
require __DIR__ . '/enforce_api_rbac.php';
}
}