seedproject-web/api/app/Controllers/PublicController.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.6 KiB
PHP

<?php
namespace App\Controllers;
/** Base for public (browser-callable) endpoints: origin allowlist + rate limit. */
class PublicController extends JsonController
{
/**
* Cross-origin browser requests send Origin; if present and not allowlisted -> false.
* Same-origin GET / non-browser callers omit Origin -> allowed.
*/
protected function checkOrigin(): bool
{
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
if ($origin === '') {
return true; // same-origin GET or server-side caller
}
$allowed = array_filter(array_map('trim', explode(',', defined('ALLOWED_ORIGINS') ? ALLOWED_ORIGINS : '')));
if (empty($allowed)) {
return true; // not configured (dev)
}
$originHost = parse_url($origin, PHP_URL_HOST);
foreach ($allowed as $a) {
$host = parse_url($a, PHP_URL_HOST) ?: $a;
if ($originHost && strcasecmp($originHost, $host) === 0) {
header('Access-Control-Allow-Origin: ' . $origin);
return true;
}
}
return false;
}
/** Guard helper: enforce origin + rate limit, or emit the error envelope and stop. */
protected function guardPublic(string $key, int $max = 60, int $window = 60): void
{
if (!$this->checkOrigin()) {
$this->json(null, 403, ['code' => 'forbidden_origin', 'message' => 'Origin not allowed']);
}
if ($this->throttle($key, $max, $window)) {
$this->json(null, 429, ['code' => 'rate_limited', 'message' => 'Too many requests']);
}
}
}