feat: add home page and asset-serving handlers

This commit is contained in:
Keith Solomon
2026-07-06 19:02:06 -05:00
parent 0f4a220e70
commit 3e4184f08d
4 changed files with 212 additions and 0 deletions
+74
View File
@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace BattleForge\Http\Handlers;
use BattleForge\Http\Request;
use BattleForge\Http\Response;
final class GetAssets
{
public function __construct(
private readonly string $placeholderDir,
private readonly string $uploadsRoot,
) {
}
/** @param array<string, string> $params */
public function handle(Request $request, array $params): Response
{
$kind = $params['kind'] ?? '';
$filename = $params['filename'] ?? '';
if ($kind === 'placeholders') {
return $this->serveFrom($this->placeholderDir . '/' . $filename);
}
if ($kind === 'uploads') {
$userToken = $params['userToken'] ?? '';
$requestToken = $request->cookies['__uploads_token'] ?? '';
if ($userToken === '' || $userToken !== $requestToken) {
return Response::html(404, '<h1>Not found</h1>');
}
return $this->serveFrom($this->uploadsRoot . '/' . $userToken . '/' . $filename);
}
return Response::html(404, '<h1>Not found</h1>');
}
private function serveFrom(string $path): Response
{
if (!is_file($path)) {
return Response::html(404, '<h1>Not found</h1>');
}
$body = file_get_contents($path);
if ($body === false) {
return Response::html(500, '<h1>Read error</h1>');
}
$ext = strtolower(pathinfo($path, PATHINFO_EXTENSION));
$contentType = match ($ext) {
'png' => 'image/png',
'jpg', 'jpeg' => 'image/jpeg',
'webp' => 'image/webp',
'gif' => 'image/gif',
default => 'application/octet-stream',
};
return new Response(200, ['Content-Type' => $contentType] + self::securityHeaders(), $body);
}
/** @return array<string, string> */
private static function securityHeaders(): array
{
return [
'X-Content-Type-Options' => 'nosniff',
'Referrer-Policy' => 'same-origin',
'Content-Security-Policy' => "default-src 'self'; img-src 'self' data:; style-src 'self'",
];
}
}
+42
View File
@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace BattleForge\Http\Handlers;
use BattleForge\Http\Request;
use BattleForge\Http\Response;
final class GetHomePage
{
/** @param array<string, string> $params */
public function handle(Request $request, array $params): Response
{
$body = <<<'HTML'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>BattleForge</title>
<link rel="stylesheet" href="/assets/styles.css">
<meta name="csrf-token" content="{{ csrf }}">
</head>
<body>
<h1>BattleForge</h1>
<p><a href="/scenarios/new/edit/team">New scenario</a></p>
<h2>Recent scenarios</h2>
<div id="recent"><p class="bf-empty">No saved scenarios yet.</p></div>
<script type="module" src="/js/storage.js"></script>
</body>
</html>
HTML;
// The CSRF token placeholder is filled by the front controller (Task 16)
// before the body is sent. The handler does not see the cookie or the
// secret; it just receives a pre-issued token from the front controller.
$token = $request->cookies['__csrf'] ?? '';
$body = str_replace('{{ csrf }}', htmlspecialchars($token, ENT_QUOTES, 'UTF-8'), $body);
return Response::html(200, $body);
}
}