Builds the /api Foundation into the base (per api/.memory/foundation-plan.md):
- app/Services/Installer.php DB test + dump.sql import + crypto keys + /api config + lock
- app/Services/Migrator.php versioned db/migrations/*.sql runner (+ migrations table)
- commands/InstallCommand.php (app:install), commands/MigrateCommand.php (db:migrate)
- app/Controllers/{JsonController,PublicController,ApiController} envelope + two-tier auth
- public/controllers/{health,admin}.php GET /api/health (public), /api/admin/ping (bearer)
- db/migrations/001_*.sql smoke migration
- install/controllers/index.php web wizard now delegates to Installer (path bugs fixed, lock)
- console + composer.json register commands / add commands to classmap
- app/src/pages/api-health-test.astro browser round-trip proof page
Verified (no DB): composer install OK; php console lists app:install + db:migrate;
PSR-4 classes autoload; CLI fails gracefully (validation, bad DB, missing config) with
no artifacts left; app builds 14 pages. Live DB + HTTP round-trip pending a served instance.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SYHWLHihq3v9nxNwoPCKSn
47 lines
1.9 KiB
PHP
47 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.
|
|
}
|
|
}
|