seedproject-web/api/app/Controllers/ApiController.php

48 lines
1.9 KiB
PHP
Raw Permalink Normal View History

<?php
namespace App\Controllers;
/** Base for privileged endpoints: bearer ADMIN_TOKEN or api_auth API key. */
class ApiController extends JsonController
{
protected ?array $apiUser = null; // set when an api_auth key authenticates
/** Extract a token from Authorization: Bearer, ?apikey=, or X-Api-Key. */
private function bearer(): string
{
$h = $_SERVER['HTTP_AUTHORIZATION'] ?? ($_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ?? '');
if (stripos($h, 'Bearer ') === 0) {
return trim(substr($h, 7));
}
return $_SERVER['HTTP_X_API_KEY'] ?? ($_GET['apikey'] ?? '');
}
/** Enforce authentication; emits 401 and stops on failure. Also basic per-minute throttle. */
protected function requireAuth(int $max = 120, int $window = 60): void
{
$token = $this->bearer();
if ($token === '') {
$this->json(null, 401, ['code' => 'unauthorized', 'message' => 'Missing bearer token']);
}
// First-party admin token (constant-time compare).
if (defined('ADMIN_TOKEN') && hash_equals(ADMIN_TOKEN, $token)) {
if ($this->throttle('admin', $max, $window)) {
$this->json(null, 429, ['code' => 'rate_limited', 'message' => 'Too many requests']);
}
return;
}
// Programmatic api_auth key.
$row = \Db::getRow(
"SELECT `id`, `userid`, `active` FROM `api_auth` WHERE `apikey` = ? LIMIT 1",
[$token]
);
if (!$row || (int) $row['active'] !== 1) {
$this->json(null, 401, ['code' => 'unauthorized', 'message' => 'Invalid API key']);
}
$this->apiUser = $row;
if ($this->throttle('apikey:' . $row['id'], $max, $window)) {
$this->json(null, 429, ['code' => 'rate_limited', 'message' => 'Too many requests']);
}
// NOTE: monthly/plan quota enforcement (api_plans/api_usage) is deferred to a later sub-project.
}
}