conn = $conn; } /** * Generate a new API key * * @param string $keyName Descriptive name for the key * @param int $createdBy User ID who created the key * @param int|null $expiresInDays Number of days until expiration (null for no expiration) * @param string $scope Access scope: 'read' or 'read_write' (default 'read_write') * @return array Array with 'success', 'api_key' (plaintext), 'key_prefix', 'scope', 'error' */ public function createKey($keyName, $createdBy, $expiresInDays = null, $scope = 'read_write') { // Validate the requested scope — only the two known values are allowed if (!in_array($scope, ['read', 'read_write'], true)) { return [ 'success' => false, 'error' => "Invalid scope: must be 'read' or 'read_write'" ]; } // Generate random API key (32 bytes = 64 hex characters) $apiKey = bin2hex(random_bytes(32)); // Create key prefix (first 8 characters) for identification $keyPrefix = substr($apiKey, 0, 8); // Hash the API key for storage $keyHash = hash('sha256', $apiKey); // Calculate expiration date if specified $expiresAt = null; if ($expiresInDays !== null) { $expiresAt = date('Y-m-d H:i:s', strtotime("+$expiresInDays days")); } // Insert API key into database $stmt = $this->conn->prepare( "INSERT INTO api_keys (key_name, key_hash, key_prefix, scope, created_by, expires_at) " . "VALUES (?, ?, ?, ?, ?, ?)" ); $stmt->bind_param("ssssis", $keyName, $keyHash, $keyPrefix, $scope, $createdBy, $expiresAt); if ($stmt->execute()) { $keyId = $this->conn->insert_id; $stmt->close(); return [ 'success' => true, 'api_key' => $apiKey, // Return plaintext key ONCE 'key_prefix' => $keyPrefix, 'key_id' => $keyId, 'scope' => $scope, 'expires_at' => $expiresAt ]; } else { $error = $this->conn->error; $stmt->close(); return [ 'success' => false, 'error' => $error ]; } } /** * Validate an API key * * @param string $apiKey Plaintext API key to validate * @return array|null API key record if valid, null if invalid */ public function validateKey($apiKey) { if (empty($apiKey)) { return null; } // Hash the provided key $keyHash = hash('sha256', $apiKey); // Query for matching key $stmt = $this->conn->prepare( "SELECT * FROM api_keys WHERE key_hash = ? AND is_active = 1" ); $stmt->bind_param("s", $keyHash); $stmt->execute(); $result = $stmt->get_result(); if ($result->num_rows === 0) { $stmt->close(); return null; } $keyData = $result->fetch_assoc(); $stmt->close(); // Ensure a scope is always present. On an un-migrated database the column // does not exist yet (or is null), in which case we treat the key as // full-access so existing integrations keep working. if (!isset($keyData['scope']) || $keyData['scope'] === null || $keyData['scope'] === '') { $keyData['scope'] = 'read_write'; } // Check expiration if ($keyData['expires_at'] !== null) { $expiresAt = strtotime($keyData['expires_at']); if ($expiresAt < time()) { return null; // Key has expired } } // Update last_used timestamp $this->updateLastUsed($keyData['api_key_id']); return $keyData; } /** * Update last_used timestamp for an API key * * @param int $keyId API key ID * @return bool Success status */ private function updateLastUsed($keyId) { $stmt = $this->conn->prepare("UPDATE api_keys SET last_used = NOW() WHERE api_key_id = ?"); $stmt->bind_param("i", $keyId); $success = $stmt->execute(); $stmt->close(); return $success; } /** * Revoke an API key (set is_active to false) * * @param int $keyId API key ID * @return bool Success status */ public function revokeKey($keyId) { $stmt = $this->conn->prepare("UPDATE api_keys SET is_active = 0 WHERE api_key_id = ?"); $stmt->bind_param("i", $keyId); $success = $stmt->execute(); $stmt->close(); return $success; } /** * Delete an API key permanently * * @param int $keyId API key ID * @return bool Success status */ public function deleteKey($keyId) { $stmt = $this->conn->prepare("DELETE FROM api_keys WHERE api_key_id = ?"); $stmt->bind_param("i", $keyId); $success = $stmt->execute(); $stmt->close(); return $success; } /** * Get a page of API keys (for admin panel) * * Active keys are listed first, then newest first within each group. * * @param int $page 1-based page number * @param int $perPage Rows per page * @return array ['keys' => array, 'total' => int, 'page' => int, 'perPage' => int] */ public function getAllKeys($page = 1, $perPage = 20) { // Normalise pagination inputs $page = max(1, (int)$page); $perPage = (int)$perPage; if ($perPage < 1) { $perPage = 20; } $offset = ($page - 1) * $perPage; // Total count for pagination controls $total = 0; $countResult = $this->conn->query("SELECT COUNT(*) AS total FROM api_keys"); if ($countResult) { $countRow = $countResult->fetch_assoc(); $total = (int)($countRow['total'] ?? 0); $countResult->free(); } $stmt = $this->conn->prepare( "SELECT ak.*, u.username, u.display_name FROM api_keys ak LEFT JOIN users u ON ak.created_by = u.user_id ORDER BY ak.is_active DESC, ak.created_at DESC LIMIT ? OFFSET ?" ); $stmt->bind_param("ii", $perPage, $offset); $stmt->execute(); $result = $stmt->get_result(); $keys = []; while ($row = $result->fetch_assoc()) { // Remove key_hash from response for security unset($row['key_hash']); $keys[] = $row; } $stmt->close(); return [ 'keys' => $keys, 'total' => $total, 'page' => $page, 'perPage' => $perPage ]; } /** * Get API key by ID * * @param int $keyId API key ID * @return array|null API key record (without hash) or null if not found */ public function getKeyById($keyId) { $stmt = $this->conn->prepare( "SELECT ak.*, u.username, u.display_name FROM api_keys ak LEFT JOIN users u ON ak.created_by = u.user_id WHERE ak.api_key_id = ?" ); $stmt->bind_param("i", $keyId); $stmt->execute(); $result = $stmt->get_result(); if ($result->num_rows > 0) { $key = $result->fetch_assoc(); // Remove key_hash from response for security unset($key['key_hash']); $stmt->close(); return $key; } $stmt->close(); return null; } /** * Get keys created by a specific user * * @param int $userId User ID * @return array Array of API key records */ public function getKeysByUser($userId) { $stmt = $this->conn->prepare( "SELECT * FROM api_keys WHERE created_by = ? ORDER BY created_at DESC" ); $stmt->bind_param("i", $userId); $stmt->execute(); $result = $stmt->get_result(); $keys = []; while ($row = $result->fetch_assoc()) { // Remove key_hash from response for security unset($row['key_hash']); $keys[] = $row; } $stmt->close(); return $keys; } }