Skip to content

Commit ab5f73e

Browse files
committed
Add room context to User and getCurrentUserPlayer method
- Add room property to User entity for tracking current room context - Add getCurrentUserPlayer method to PlayerRepository - Update all Voters (PlayerVoter, MissionVoter, RoomVoter) to use getCurrentUserPlayer - Create database migration to add room_id to users table The room property on User tracks which room context the user is currently operating in. This allows for proper authorization checks in Voters by getting the current player based on the user's room context. Changes: - User entity: Added nullable room property with SET NULL on delete - PlayerRepository: Added getCurrentUserPlayer(User) method - DoctrinePlayerRepository: Implemented getCurrentUserPlayer logic - PlayerVoter: Use getCurrentUserPlayer for authorization - MissionVoter: Use getCurrentUserPlayer for authorization - RoomVoter: Use getCurrentUserPlayer for authorization - Migration: Add room_id column to users table with foreign key to room
1 parent 0beb5bb commit ab5f73e

File tree

7 files changed

+133
-19
lines changed

7 files changed

+133
-19
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace DoctrineMigrations;
6+
7+
use Doctrine\DBAL\Schema\Schema;
8+
use Doctrine\Migrations\AbstractMigration;
9+
10+
/**
11+
* Migration: Add room property to User entity for room context tracking
12+
*/
13+
final class Version20251108171358 extends AbstractMigration
14+
{
15+
public function getDescription(): string
16+
{
17+
return 'Add room_id column to users table to track user\'s current room context';
18+
}
19+
20+
public function up(Schema $schema): void
21+
{
22+
// Add room_id column to users table
23+
$this->addSql('ALTER TABLE users ADD room_id INTEGER DEFAULT NULL');
24+
$this->addSql('ALTER TABLE users ADD CONSTRAINT FK_1483A5E954177093 FOREIGN KEY (room_id) REFERENCES room (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE');
25+
$this->addSql('CREATE INDEX IDX_1483A5E954177093 ON users (room_id)');
26+
}
27+
28+
public function down(Schema $schema): void
29+
{
30+
// Remove room_id column from users table
31+
$this->addSql('ALTER TABLE users DROP CONSTRAINT FK_1483A5E954177093');
32+
$this->addSql('DROP INDEX IDX_1483A5E954177093');
33+
$this->addSql('ALTER TABLE users DROP COLUMN room_id');
34+
}
35+
}

src/Domain/Player/PlayerRepository.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use App\Domain\Player\Entity\Player;
88
use App\Domain\Room\Entity\Room;
9+
use App\Domain\User\Entity\User;
910
use App\Infrastructure\Persistence\BaseRepository;
1011

1112
/** @extends BaseRepository<Player> */
@@ -21,4 +22,10 @@ public function findPlayersByRoomAndName(Room $room, string $name): array;
2122
* Find the killer of a player (the player who has this player as their target).
2223
*/
2324
public function findKiller(Player $player): ?Player;
25+
26+
/**
27+
* Get the current player for a user based on the user's current room context.
28+
* Returns the player belonging to the user that is in the user's current room.
29+
*/
30+
public function getCurrentUserPlayer(User $user): ?Player;
2431
}

src/Domain/User/Entity/User.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace App\Domain\User\Entity;
66

77
use App\Domain\Player\Entity\Player;
8+
use App\Domain\Room\Entity\Room;
89
use Doctrine\Common\Collections\ArrayCollection;
910
use Doctrine\Common\Collections\Collection;
1011
use Doctrine\ORM\Mapping as ORM;
@@ -51,6 +52,10 @@ class User implements UserInterface
5152
#[ORM\Column(type: 'string', length: 255, nullable: true)]
5253
private ?string $appleId = null;
5354

55+
#[ORM\ManyToOne(targetEntity: Room::class)]
56+
#[ORM\JoinColumn(name: 'room_id', nullable: true, onDelete: 'SET NULL')]
57+
private ?Room $room = null;
58+
5459
public function __construct()
5560
{
5661
$this->players = new ArrayCollection();
@@ -177,4 +182,16 @@ public function setAppleId(?string $appleId): self
177182

178183
return $this;
179184
}
185+
186+
public function getRoom(): ?Room
187+
{
188+
return $this->room;
189+
}
190+
191+
public function setRoom(?Room $room): self
192+
{
193+
$this->room = $room;
194+
195+
return $this;
196+
}
180197
}

src/Infrastructure/Persistence/Doctrine/Repository/DoctrinePlayerRepository.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use App\Domain\Player\Entity\Player;
88
use App\Domain\Player\PlayerRepository;
99
use App\Domain\Room\Entity\Room;
10+
use App\Domain\User\Entity\User;
1011
use Doctrine\ORM\EntityManagerInterface;
1112

1213
/** @extends DoctrineBaseRepository<Player> */
@@ -36,4 +37,22 @@ public function findKiller(Player $player): ?Player
3637
{
3738
return $this->findOneBy(['target' => $player]);
3839
}
40+
41+
/**
42+
* Get the current player for a user based on the user's current room context.
43+
* Returns the player belonging to the user that is in the user's current room.
44+
*/
45+
public function getCurrentUserPlayer(User $user): ?Player
46+
{
47+
$room = $user->getRoom();
48+
49+
if ($room === null) {
50+
return null;
51+
}
52+
53+
return $this->findOneBy([
54+
'user' => $user,
55+
'room' => $room,
56+
]);
57+
}
3958
}

src/Infrastructure/Security/Voters/MissionVoter.php

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
use App\Domain\Mission\Entity\Mission;
88
use App\Domain\Player\Entity\Player;
9+
use App\Domain\Player\PlayerRepository;
10+
use App\Domain\User\Entity\User;
911
use Symfony\Bundle\SecurityBundle\Security;
1012
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
1113
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
@@ -16,8 +18,10 @@ class MissionVoter extends Voter
1618
public const VIEW_MISSION = 'view_mission';
1719
public const CREATE_MISSION = 'create_mission';
1820

19-
public function __construct(private readonly Security $security)
20-
{
21+
public function __construct(
22+
private readonly Security $security,
23+
private readonly PlayerRepository $playerRepository,
24+
) {
2125
}
2226

2327
protected function supports(string $attribute, mixed $subject): bool
@@ -31,16 +35,22 @@ protected function supports(string $attribute, mixed $subject): bool
3135

3236
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
3337
{
34-
$player = $token->getUser();
38+
$user = $token->getUser();
39+
40+
if (!$user instanceof User) {
41+
return false;
42+
}
43+
44+
$currentPlayer = $this->playerRepository->getCurrentUserPlayer($user);
3545

36-
if (!$player instanceof Player) {
46+
if ($currentPlayer === null) {
3747
return false;
3848
}
3949

4050
return match ($attribute) {
41-
self::VIEW_MISSION => $this->canView($subject, $player),
42-
self::EDIT_MISSION => $this->canEdit($subject, $player),
43-
self::CREATE_MISSION => $this->canPost($player),
51+
self::VIEW_MISSION => $this->canView($subject, $currentPlayer),
52+
self::EDIT_MISSION => $this->canEdit($subject, $currentPlayer),
53+
self::CREATE_MISSION => $this->canPost($currentPlayer),
4454
default => throw new \LogicException('This code should not be reached'),
4555
};
4656
}

src/Infrastructure/Security/Voters/PlayerVoter.php

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,36 @@
55
namespace App\Infrastructure\Security\Voters;
66

77
use App\Domain\Player\Entity\Player;
8+
use App\Domain\Player\PlayerRepository;
9+
use App\Domain\User\Entity\User;
810
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
911
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
1012

1113
class PlayerVoter extends Voter
1214
{
1315
public const string EDIT_PLAYER = 'edit_player';
1416

17+
public function __construct(
18+
private readonly PlayerRepository $playerRepository,
19+
) {
20+
}
21+
1522
protected function supports(string $attribute, mixed $subject): bool
1623
{
1724
return $attribute === self::EDIT_PLAYER && $subject instanceof Player;
1825
}
1926

2027
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
2128
{
22-
$playerInSession = $token->getUser();
29+
$user = $token->getUser();
30+
31+
if (!$user instanceof User) {
32+
return false;
33+
}
34+
35+
$currentPlayer = $this->playerRepository->getCurrentUserPlayer($user);
2336

24-
if (!$playerInSession instanceof Player) {
37+
if ($currentPlayer === null) {
2538
return false;
2639
}
2740

@@ -32,13 +45,13 @@ protected function voteOnAttribute(string $attribute, mixed $subject, TokenInter
3245
throw new \LogicException('This code should not be reached');
3346
}
3447

35-
return $this->canEdit($player, $playerInSession);
48+
return $this->canEdit($player, $currentPlayer);
3649
}
3750

38-
private function canEdit(Player $player, Player $playerInSession): bool
51+
private function canEdit(Player $player, Player $currentPlayer): bool
3952
{
40-
return $player === $playerInSession
41-
|| ($player->getRoom() === $playerInSession->getRoom()
42-
&& $player->getRoom()?->getAdmin() === $playerInSession);
53+
return $player === $currentPlayer
54+
|| ($player->getRoom() === $currentPlayer->getRoom()
55+
&& $player->getRoom()?->getAdmin() === $currentPlayer);
4356
}
4457
}

src/Infrastructure/Security/Voters/RoomVoter.php

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
namespace App\Infrastructure\Security\Voters;
66

77
use App\Domain\Player\Entity\Player;
8+
use App\Domain\Player\PlayerRepository;
89
use App\Domain\Room\Entity\Room;
10+
use App\Domain\User\Entity\User;
911
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
1012
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
1113

@@ -15,6 +17,11 @@ class RoomVoter extends Voter
1517
public const VIEW_ROOM = 'view_room';
1618
public const CREATE_ROOM = 'create_room';
1719

20+
public function __construct(
21+
private readonly PlayerRepository $playerRepository,
22+
) {
23+
}
24+
1825
protected function supports(string $attribute, mixed $subject): bool
1926
{
2027
if (!in_array($attribute, [self::EDIT_ROOM, self::VIEW_ROOM, self::CREATE_ROOM], true)) {
@@ -26,20 +33,26 @@ protected function supports(string $attribute, mixed $subject): bool
2633

2734
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
2835
{
29-
$player = $token->getUser();
36+
$user = $token->getUser();
3037

3138
if ($attribute === self::EDIT_ROOM && $token->getUserIdentifier() === '') {
3239
return true;
3340
}
3441

35-
if (!$player instanceof Player) {
42+
if (!$user instanceof User) {
43+
return false;
44+
}
45+
46+
$currentPlayer = $this->playerRepository->getCurrentUserPlayer($user);
47+
48+
if ($currentPlayer === null) {
3649
return false;
3750
}
3851

3952
return match ($attribute) {
40-
self::VIEW_ROOM => $this->canView($subject, $player),
41-
self::EDIT_ROOM => $this->canEdit($subject, $player),
42-
self::CREATE_ROOM => $this->canCreate($player),
53+
self::VIEW_ROOM => $this->canView($subject, $currentPlayer),
54+
self::EDIT_ROOM => $this->canEdit($subject, $currentPlayer),
55+
self::CREATE_ROOM => $this->canCreate($currentPlayer),
4356
default => throw new \LogicException('This code should not be reached'),
4457
};
4558
}

0 commit comments

Comments
 (0)