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
42 lines
1.5 KiB
PHP
42 lines
1.5 KiB
PHP
<?php
|
|
namespace App\Controllers;
|
|
|
|
/** Base for JSON endpoints: response envelope + IP throttle. Extends core \Controller. */
|
|
class JsonController extends \Controller
|
|
{
|
|
/** Emit { ok, data, error } with the right HTTP status, then stop. */
|
|
protected function json($data = null, int $status = 200, ?array $error = null): void
|
|
{
|
|
http_response_code($status);
|
|
header('Content-Type: application/json; charset=UTF-8');
|
|
echo json_encode([
|
|
'ok' => $error === null,
|
|
'data' => $data,
|
|
'error' => $error, // ['code' => ..., 'message' => ...] or null
|
|
]);
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Returns true when the caller has EXCEEDED $max hits on $key within $window seconds.
|
|
* Reuses the api_requests table (no new table needed).
|
|
*/
|
|
protected function throttle(string $key, int $max, int $window): bool
|
|
{
|
|
$ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
|
|
$count = (int) \Db::getValue(
|
|
"SELECT COUNT(*) FROM `api_requests`
|
|
WHERE `requesting_ip` = ? AND `request` = ?
|
|
AND `created_date` > (NOW() - INTERVAL ? SECOND)",
|
|
[$ip, $key, $window]
|
|
);
|
|
\Db::insert('api_requests', [
|
|
'requesting_ip' => $ip,
|
|
'request' => $key,
|
|
'service' => 'foundation',
|
|
'domainURI' => $_SERVER['HTTP_HOST'] ?? '',
|
|
'created_date' => date('Y-m-d H:i:s'),
|
|
]);
|
|
return $count >= $max;
|
|
}
|
|
}
|