48 lines
1.9 KiB
PHP
48 lines
1.9 KiB
PHP
|
|
<?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.
|
||
|
|
}
|
||
|
|
}
|