Skip to content

Commit c5f4a89

Browse files
committed
Refactor user roles
Signed-off-by: Tim Goudriaan <tim@codedmonkey.com>
1 parent ebff483 commit c5f4a89

7 files changed

Lines changed: 107 additions & 52 deletions

File tree

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
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+
final class Version20260112221847 extends AbstractMigration
11+
{
12+
public function getDescription(): string
13+
{
14+
return 'Refactor user roles from JSON array to single role enum';
15+
}
16+
17+
public function up(Schema $schema): void
18+
{
19+
// Add new role column as nullable
20+
$this->addSql('ALTER TABLE "user" ADD role VARCHAR(64) DEFAULT NULL');
21+
22+
// Migrate existing data: extract the highest privilege role from the roles array
23+
$this->addSql(<<<'SQL'
24+
UPDATE "user"
25+
SET role = CASE
26+
WHEN roles::text LIKE '%ROLE_SUPER_ADMIN%' THEN 'ROLE_SUPER_ADMIN'
27+
WHEN roles::text LIKE '%ROLE_ADMIN%' THEN 'ROLE_ADMIN'
28+
ELSE 'ROLE_USER'
29+
END
30+
SQL);
31+
32+
// Make role column NOT NULL
33+
$this->addSql('ALTER TABLE "user" ALTER COLUMN role SET NOT NULL');
34+
35+
// Drop the old roles column
36+
$this->addSql('ALTER TABLE "user" DROP roles');
37+
}
38+
39+
public function down(Schema $schema): void
40+
{
41+
// Add back the roles column
42+
$this->addSql('ALTER TABLE "user" ADD roles JSON DEFAULT NULL');
43+
44+
// Migrate data back: convert single role to array
45+
$this->addSql(<<<'SQL'
46+
UPDATE "user"
47+
SET roles = CASE
48+
WHEN role = 'ROLE_SUPER_ADMIN' THEN '["ROLE_SUPER_ADMIN"]'::json
49+
WHEN role = 'ROLE_ADMIN' THEN '["ROLE_ADMIN"]'::json
50+
ELSE '[]'::json
51+
END
52+
SQL);
53+
54+
// Make roles column NOT NULL
55+
$this->addSql('ALTER TABLE "user" ALTER COLUMN roles SET NOT NULL');
56+
57+
// Drop the new role column
58+
$this->addSql('ALTER TABLE "user" DROP role');
59+
}
60+
}

src/Controller/Dashboard/DashboardSecurityController.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use CodedMonkey\Dirigent\Doctrine\Entity\User;
66
use CodedMonkey\Dirigent\Doctrine\Repository\UserRepository;
7+
use CodedMonkey\Dirigent\Entity\UserRole;
78
use CodedMonkey\Dirigent\Form\RegistrationFormType;
89
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
910
use Symfony\Bundle\SecurityBundle\Security;
@@ -56,7 +57,7 @@ public function register(Request $request, Security $security): Response
5657
if ($form->isSubmitted() && $form->isValid()) {
5758
// The first user gets owner privileges
5859
if ($noUsers) {
59-
$user->setRoles(['ROLE_SUPER_ADMIN', 'ROLE_USER']);
60+
$user->setRole(UserRole::Owner);
6061
}
6162

6263
$this->userRepository->save($user, true);

src/Controller/Dashboard/DashboardUserController.php

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace CodedMonkey\Dirigent\Controller\Dashboard;
44

55
use CodedMonkey\Dirigent\Doctrine\Entity\User;
6+
use CodedMonkey\Dirigent\Entity\UserRole;
67
use CodedMonkey\Dirigent\Form\NewPasswordType;
78
use EasyCorp\Bundle\EasyAdminBundle\Attribute\AdminRoute;
89
use EasyCorp\Bundle\EasyAdminBundle\Config\Action;
@@ -55,19 +56,13 @@ public function configureFields(string $pageName): iterable
5556
->setFormType(PasswordType::class)
5657
->setFormTypeOption('constraints', NewPasswordType::constraints())
5758
->onlyOnForms();
58-
yield ChoiceField::new('roles')
59-
->setChoices([
60-
'User' => 'ROLE_USER',
61-
'Admin' => 'ROLE_ADMIN',
62-
'Owner' => 'ROLE_SUPER_ADMIN',
63-
])
59+
yield ChoiceField::new('role')
60+
->setChoices(UserRole::cases())
6461
->renderAsBadges([
65-
'ROLE_USER' => 'primary',
66-
'ROLE_ADMIN' => 'success',
67-
'ROLE_SUPER_ADMIN' => 'success',
62+
UserRole::User->value => 'primary',
63+
UserRole::Admin->value => 'success',
64+
UserRole::Owner->value => 'success',
6865
])
69-
->renderExpanded()
70-
->allowMultipleChoices()
7166
->setSortable(false);
7267
yield BooleanField::new('totpAuthenticationEnabled', 'Multi-factor authentication')
7368
->setHelp('form.user.help.totp-authentication-enabled')

src/Doctrine/DataFixtures/AppFixtures.php

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use CodedMonkey\Dirigent\Doctrine\Entity\Registry;
66
use CodedMonkey\Dirigent\Doctrine\Entity\RegistryPackageMirroring;
77
use CodedMonkey\Dirigent\Doctrine\Entity\User;
8+
use CodedMonkey\Dirigent\Entity\UserRole;
89
use Doctrine\Bundle\FixturesBundle\Fixture;
910
use Doctrine\Persistence\ObjectManager;
1011

@@ -18,7 +19,7 @@ public function load(ObjectManager $manager): void
1819
$user->setUsername($userData['username']);
1920
$user->setName($userData['name']);
2021
$user->setEmail($userData['email']);
21-
$user->setRoles($userData['roles']);
22+
$user->setRole($userData['role']);
2223
$user->setPlainPassword($userData['password']);
2324

2425
$manager->persist($user);
@@ -45,23 +46,23 @@ private function getUsers(): \Generator
4546
'username' => 'owner',
4647
'name' => 'Owner',
4748
'email' => 'owner@example.com',
48-
'roles' => ['ROLE_SUPER_ADMIN'],
49+
'role' => UserRole::Owner,
4950
'password' => 'PlainPassword99',
5051
];
5152

5253
yield [
5354
'username' => 'admin',
5455
'name' => 'Admin User',
5556
'email' => 'admin@example.com',
56-
'roles' => ['ROLE_ADMIN'],
57+
'role' => UserRole::Admin,
5758
'password' => 'PlainPassword99',
5859
];
5960

6061
yield [
6162
'username' => 'user',
6263
'name' => 'Regular User',
6364
'email' => 'user@example.com',
64-
'roles' => [],
65+
'role' => UserRole::User,
6566
'password' => 'PlainPassword99',
6667
];
6768
}

src/Doctrine/Entity/User.php

Lines changed: 13 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
namespace CodedMonkey\Dirigent\Doctrine\Entity;
44

55
use CodedMonkey\Dirigent\Doctrine\Repository\UserRepository;
6+
use CodedMonkey\Dirigent\Entity\UserRole;
7+
use Doctrine\DBAL\Types\Types;
68
use Doctrine\ORM\Mapping\Column;
79
use Doctrine\ORM\Mapping\Entity;
810
use Doctrine\ORM\Mapping\GeneratedValue;
@@ -34,8 +36,8 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, TwoFact
3436
#[Column(length: 180, nullable: true)]
3537
private ?string $email = null;
3638

37-
#[Column]
38-
private array $roles = [];
39+
#[Column(type: Types::STRING, length: 64, enumType: UserRole::class)]
40+
private UserRole $role = UserRole::User;
3941

4042
#[Column]
4143
private ?string $password = null;
@@ -82,15 +84,17 @@ public function setEmail(?string $email): void
8284

8385
public function getRoles(): array
8486
{
85-
$roles = $this->roles;
86-
$roles[] = 'ROLE_USER';
87+
return [$this->role->value];
88+
}
8789

88-
return array_unique($roles);
90+
public function getRole(): UserRole
91+
{
92+
return $this->role;
8993
}
9094

91-
public function setRoles(array $roles): void
95+
public function setRole(UserRole $role): void
9296
{
93-
$this->roles = $roles;
97+
$this->role = $role;
9498
}
9599

96100
public function getPassword(): ?string
@@ -143,38 +147,12 @@ public function eraseCredentials(): void
143147

144148
public function isAdmin(): bool
145149
{
146-
return in_array('ROLE_ADMIN', $this->roles, true) || in_array('ROLE_SUPER_ADMIN', $this->roles, true);
150+
return $this->role->isAdmin();
147151
}
148152

149153
public function isSuperAdmin(): bool
150154
{
151-
return in_array('ROLE_SUPER_ADMIN', $this->roles, true);
152-
}
153-
154-
public function setAdmin(bool $admin): void
155-
{
156-
if ($admin) {
157-
if (!in_array('ROLE_ADMIN', $this->roles, true)) {
158-
$this->roles[] = 'ROLE_ADMIN';
159-
}
160-
} else {
161-
if (false !== $key = array_search('ROLE_ADMIN', $this->roles, true)) {
162-
unset($this->roles[$key]);
163-
}
164-
}
165-
}
166-
167-
public function setSuperAdmin(bool $admin): void
168-
{
169-
if ($admin) {
170-
if (!in_array('ROLE_SUPER_ADMIN', $this->roles, true)) {
171-
$this->roles[] = 'ROLE_SUPER_ADMIN';
172-
}
173-
} else {
174-
if (false !== $key = array_search('ROLE_SUPER_ADMIN', $this->roles, true)) {
175-
unset($this->roles[$key]);
176-
}
177-
}
155+
return $this->role->isSuperAdmin();
178156
}
179157

180158
public function isTotpAuthenticationEnabled(): bool

src/Entity/UserRole.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
namespace CodedMonkey\Dirigent\Entity;
4+
5+
enum UserRole: string
6+
{
7+
case Owner = 'ROLE_SUPER_ADMIN';
8+
case Admin = 'ROLE_ADMIN';
9+
case User = 'ROLE_USER';
10+
11+
public function isAdmin(): bool
12+
{
13+
return self::Admin === $this || $this->isSuperAdmin();
14+
}
15+
16+
public function isSuperAdmin(): bool
17+
{
18+
return self::Owner === $this;
19+
}
20+
}

translations/messages.en.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ Package Mirroring: Package mirroring
3535
Password: Password
3636
Repeat new password: Repeat new password
3737
Repository url: Repository URL
38-
Roles: Roles
38+
Role: Role
3939
Token: Token
4040
Type: Type
4141
URL: URL

0 commit comments

Comments
 (0)