seedproject-web/api/app/Controllers/JsonController.php
Carlos Arias 1fdfc3174b feat(api): Foundation — installer, migrations, two-tier auth, health round-trip
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
2026-07-04 23:23:51 +00:00

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;
}
}