feat: add home page and asset-serving handlers
This commit is contained in:
@@ -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'",
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user