Goal-Calibration/app/Node/NodeController.php
Yisroel Baum 7473af4163
enforce text ownership on node endpoints
getNodesOfText, createNode, and bulkCreateNodes now require
the session user, look up the target text, and respond 403
unless the user owns the text or is an admin. paves the way
for moving these endpoints out of the admin-only group.
2026-05-02 21:45:47 +03:00

218 lines
6.8 KiB
PHP

<?php
namespace App\Node;
use App\Exceptions\BadRequestException;
use App\Node\UseCases\BulkCreateNodesRequest;
use App\Node\NodeRepository;
use App\Node\UseCases\BulkCreateNodes;
use App\Node\UseCases\CreateNode;
use App\Node\UseCases\CreateNodeRequest;
use App\Text\Text;
use App\Text\TextRepository;
use App\User\User;
use DomainException;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
class NodeController
{
public function __construct(
private NodeRepository $nodeRepository,
private TextRepository $textRepository,
) {}
public function getNodesOfText(
Request $request,
Response $response,
int $textId,
): Response {
$user = $request->getAttribute('user');
if (!$user instanceof User) {
return $this->errorResponse(
$response,
401,
'unauthenticated'
);
}
$text = $this->textRepository->find($textId);
if ($text === null) {
return $response->withStatus(404);
}
if (!$this->userMayAccessText($user, $text)) {
return $this->errorResponse(
$response,
403,
'forbidden'
);
}
$nodes = $this->nodeRepository->findByTextId($textId);
$data = array_map(function ($node) {
return [
'id' => $node->getId(),
'title' => $node->getTitle(),
'parentNodeId' => $node->getParentNode()?->getId(),
];
}, $nodes);
$response->getBody()->write(json_encode(array_values($data)));
return $response->withHeader('Content-Type', 'application/json');
}
public function createNode(
Request $request,
Response $response,
CreateNode $createNodeUseCase,
): Response {
$user = $request->getAttribute('user');
if (!$user instanceof User) {
return $this->errorResponse(
$response,
401,
'unauthenticated'
);
}
$data = json_decode((string) $request->getBody(), true) ?? [];
$textId = isset($data['textId']) ? (int) $data['textId'] : null;
$title = $data['title'] ?? null;
$parentNodeId = isset($data['parentNodeId']) ? (int) $data['parentNodeId'] : null;
if ($textId !== null) {
$ownershipResponse = $this->checkTextOwnership(
$user,
$textId,
$response,
);
if ($ownershipResponse !== null) {
return $ownershipResponse;
}
}
try {
$node = $createNodeUseCase->execute(new CreateNodeRequest(
textId: $textId,
title: $title,
parentNodeId: $parentNodeId,
));
} catch (BadRequestException $e) {
$response->getBody()->write(json_encode(['error' => $e->getMessage()]));
return $response->withStatus(400)->withHeader('Content-Type', 'application/json');
} catch (DomainException $e) {
$response->getBody()->write(json_encode(['error' => $e->getMessage()]));
return $response->withStatus(404)->withHeader('Content-Type', 'application/json');
}
$response->getBody()->write(json_encode([
'id' => $node->getId(),
'title' => $node->getTitle(),
'parentNodeId' => $node->getParentNode()?->getId(),
]));
return $response->withStatus(201)->withHeader('Content-Type', 'application/json');
}
public function bulkCreateNodes(
Request $request,
Response $response,
BulkCreateNodes $bulkCreateNodesUseCase,
): Response {
$user = $request->getAttribute('user');
if (!$user instanceof User) {
return $this->errorResponse(
$response,
401,
'unauthenticated'
);
}
$data = json_decode((string) $request->getBody(), true) ?? [];
$textId = isset($data['textId']) ? (int) $data['textId'] : null;
$parentNodeId = isset($data['parentNodeId']) ? (int) $data['parentNodeId'] : null;
$titlePrefix = isset($data['titlePrefix']) ? (string) $data['titlePrefix'] : null;
$count = isset($data['count']) ? (int) $data['count'] : null;
if ($textId !== null) {
$ownershipResponse = $this->checkTextOwnership(
$user,
$textId,
$response,
);
if ($ownershipResponse !== null) {
return $ownershipResponse;
}
}
try {
$nodes = $bulkCreateNodesUseCase->execute(new BulkCreateNodesRequest(
textId: $textId,
parentNodeId: $parentNodeId,
titlePrefix: $titlePrefix,
count: $count,
));
} catch (BadRequestException $e) {
$response->getBody()->write(json_encode(['error' => $e->getMessage()]));
return $response->withStatus(400)->withHeader('Content-Type', 'application/json');
} catch (DomainException $e) {
$response->getBody()->write(json_encode(['error' => $e->getMessage()]));
return $response->withStatus(404)->withHeader('Content-Type', 'application/json');
}
$result = array_map(function ($node) {
return [
'id' => $node->getId(),
'title' => $node->getTitle(),
'parentNodeId' => $node->getParentNode()?->getId(),
];
}, $nodes);
$response->getBody()->write(json_encode(array_values($result)));
return $response->withStatus(201)->withHeader('Content-Type', 'application/json');
}
private function checkTextOwnership(
User $user,
int $textId,
Response $response,
): ?Response {
$text = $this->textRepository->find($textId);
if ($text === null) {
return null;
}
if (!$this->userMayAccessText($user, $text)) {
return $this->errorResponse(
$response,
403,
'forbidden'
);
}
return null;
}
private function userMayAccessText(User $user, Text $text): bool
{
if ($user->isAdmin()) {
return true;
}
return $text->getUser()->getId() === $user->getId();
}
private function errorResponse(
Response $response,
int $status,
string $message,
): Response {
$response->getBody()->write(
json_encode(['error' => $message])
);
return $response->withStatus($status)
->withHeader('Content-Type', 'application/json');
}
}