scope text endpoints by ownership

TextRepository gains findByUser; JsonTextRepository and the
fake implement filtering by stored userId. TextController
splits the list endpoint into getMyTexts (own) and
getAllTexts (admin), and getText now requires the session
user, returning 403 to non-owners while admins bypass.
This commit is contained in:
Yisroel Baum 2026-05-02 21:42:51 +03:00
parent ea6d65a77d
commit acdf703d80
Signed by: yisroelbaum
GPG key ID: 0FA60884F75520A9
4 changed files with 107 additions and 3 deletions

View file

@ -5,6 +5,7 @@ namespace App\Text;
use App\Text\Text; use App\Text\Text;
use App\Text\CreateTextDto; use App\Text\CreateTextDto;
use App\Text\TextRepository; use App\Text\TextRepository;
use App\User\User;
use App\User\UserRepository; use App\User\UserRepository;
use DomainException; use DomainException;
@ -67,6 +68,28 @@ class JsonTextRepository implements TextRepository
); );
} }
/**
* @return Text[]
*/
public function findByUser(User $user): array
{
$texts = $this->readTexts();
$userId = $user->getId();
$owned = array_filter(
$texts,
function (array $data) use ($userId) {
return $data['userId'] === $userId;
}
);
return array_map(
function (array $data) {
return $this->hydrate($data);
},
array_values($owned)
);
}
private function hydrate(array $data): Text private function hydrate(array $data): Text
{ {
$user = $this->userRepo->find($data['userId']); $user = $this->userRepo->find($data['userId']);

View file

@ -16,7 +16,7 @@ class TextController
private TextRepository $textRepository, private TextRepository $textRepository,
) {} ) {}
public function getTexts(Response $response): Response public function getAllTexts(Response $response): Response
{ {
$texts = $this->textRepository->getAll(); $texts = $this->textRepository->getAll();
@ -31,14 +31,63 @@ class TextController
return $response->withHeader('Content-Type', 'application/json'); return $response->withHeader('Content-Type', 'application/json');
} }
public function getText(Response $response, int $textId): Response public function getMyTexts(
{ Request $request,
Response $response,
): Response {
$user = $request->getAttribute('user');
if (!$user instanceof User) {
return $this->errorResponse(
$response,
401,
'unauthenticated'
);
}
$texts = $this->textRepository->findByUser($user);
$data = array_map(function ($text) {
return [
'id' => $text->getId(),
'name' => $text->getName(),
];
}, $texts);
$response->getBody()->write(json_encode($data));
return $response->withHeader('Content-Type', 'application/json');
}
public function getText(
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); $text = $this->textRepository->find($textId);
if ($text === null) { if ($text === null) {
return $response->withStatus(404); return $response->withStatus(404);
} }
if (
$text->getUser()->getId() !== $user->getId()
&& !$user->isAdmin()
) {
return $this->errorResponse(
$response,
403,
'forbidden'
);
}
$response->getBody()->write(json_encode([ $response->getBody()->write(json_encode([
'id' => $text->getId(), 'id' => $text->getId(),
'name' => $text->getName(), 'name' => $text->getName(),

View file

@ -4,6 +4,7 @@ namespace App\Text;
use App\Text\Text; use App\Text\Text;
use App\Text\CreateTextDto; use App\Text\CreateTextDto;
use App\User\User;
interface TextRepository interface TextRepository
{ {
@ -15,4 +16,9 @@ interface TextRepository
* @return Text[] * @return Text[]
*/ */
public function getAll(): array; public function getAll(): array;
/**
* @return Text[]
*/
public function findByUser(User $user): array;
} }

View file

@ -5,6 +5,7 @@ namespace Tests\Fakes;
use App\Text\CreateTextDto; use App\Text\CreateTextDto;
use App\Text\Text; use App\Text\Text;
use App\Text\TextRepository; use App\Text\TextRepository;
use App\User\User;
class FakeTextRepository implements TextRepository class FakeTextRepository implements TextRepository
{ {
@ -61,4 +62,29 @@ class FakeTextRepository implements TextRepository
array_values($this->existingTexts) array_values($this->existingTexts)
); );
} }
/**
* @return Text[]
*/
public function findByUser(User $user): array
{
$userId = $user->getId();
$owned = array_filter(
$this->existingTexts,
function (Text $text) use ($userId) {
return $text->getUser()->getId() === $userId;
}
);
return array_map(
function (Text $text) {
return new Text(
id: $text->getId(),
name: $text->getName(),
user: $text->getUser(),
);
},
array_values($owned)
);
}
} }