seedproject-web/api/app/Controllers/PublicController.php
Carlos Arias dc69d1fd0d Phase 2: media upload pipeline
- cja_media table; POST /api/media/upload (admin-gated, multipart).
- Images optimised with GD (downscale to 1600px, re-encode; WebP for
  transparency, JPG for photos). Video via ffmpeg (scale, compress, drop audio).
- Stored in app/public/media/ (source tree) so uploads survive rebuilds and are
  copied into public/ on build.
- requireAdmin() moved to PublicController base (fixes a static/non-static
  clash with AdminAuth). Type detection uses getimagesize (fileinfo ext absent).

Note: php-fpm must be in the caweb group to write app/public/media (restart
after adding www to caweb).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DoFYZY9gkGPNDqZ7NuEa9a
2026-07-23 22:45:45 +00:00

54 lines
2 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. */
/**
* Gate an admin-only endpoint. Emits 401 and stops unless the current
* session is an authenticated admin (set by AdminAuth on login). Every
* structured-content and media endpoint calls this first.
*/
protected function requireAdmin(): void
{
if (empty($_SESSION['admin']['ok'])) {
$this->json(null, 401, ['code' => 'unauthorized', 'message' => 'Admin login required']);
}
}
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']);
}
}
}