Skip to content

Commit 6108ed6

Browse files
committed
Merge branch 'security'
2 parents b7ca011 + 3cf1356 commit 6108ed6

8 files changed

Lines changed: 95 additions & 12 deletions

File tree

config/packages/security.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ security:
2525
lazy: true
2626
provider: database_users
2727
form_login:
28-
login_path: /?routeName=dashboard_login
28+
login_path: dashboard_login
2929
check_path: dashboard_login
3030
remember_me:
3131
secret: '%kernel.secret%'

config/packages/validator.yaml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,13 @@ framework:
55
#auto_mapping:
66
# App\Entity\: []
77

8+
# Disable HTTP calls to check for compromised passwords.
9+
# Dirigent should be secure, but it also needs to always work offline for now.
10+
# todo add offline mode
11+
# todo analyze if this is a good and secure way to check for compromised passwords
12+
not_compromised_password: false
13+
814
when@test:
915
framework:
1016
validation:
11-
not_compromised_password: false
17+
#not_compromised_password: false
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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 Version20250703184453 extends AbstractMigration
11+
{
12+
public function getDescription(): string
13+
{
14+
return 'Change unique user field';
15+
}
16+
17+
public function up(Schema $schema): void
18+
{
19+
$this->addSql(<<<'SQL'
20+
DROP INDEX uniq_8d93d649e7927c74
21+
SQL);
22+
$this->addSql(<<<'SQL'
23+
CREATE UNIQUE INDEX UNIQ_8D93D649F85E0677 ON "user" (username)
24+
SQL);
25+
}
26+
27+
public function down(Schema $schema): void
28+
{
29+
$this->addSql(<<<'SQL'
30+
DROP INDEX UNIQ_8D93D649F85E0677
31+
SQL);
32+
$this->addSql(<<<'SQL'
33+
CREATE UNIQUE INDEX uniq_8d93d649e7927c74 ON "user" (email)
34+
SQL);
35+
}
36+
}

src/Controller/Dashboard/DashboardSecurityController.php

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use CodedMonkey\Dirigent\Doctrine\Repository\UserRepository;
77
use CodedMonkey\Dirigent\Form\RegistrationFormType;
88
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
9+
use Symfony\Bundle\SecurityBundle\Security;
910
use Symfony\Component\HttpFoundation\Request;
1011
use Symfony\Component\HttpFoundation\Response;
1112
use Symfony\Component\Routing\Attribute\Route;
@@ -21,9 +22,7 @@ public function __construct(
2122
#[Route('/login', name: 'dashboard_login')]
2223
public function login(AuthenticationUtils $authenticationUtils): Response
2324
{
24-
$userCount = $this->userRepository->count([]);
25-
26-
if (0 === $userCount) {
25+
if ($this->userRepository->noUsers()) {
2726
return $this->redirectToRoute('dashboard_register');
2827
}
2928

@@ -38,12 +37,13 @@ public function login(AuthenticationUtils $authenticationUtils): Response
3837
}
3938

4039
#[Route('/register', name: 'dashboard_register')]
41-
public function register(Request $request): Response
40+
public function register(Request $request, Security $security): Response
4241
{
4342
$registrationEnabled = $this->getParameter('dirigent.security.registration_enabled');
44-
$userCount = $this->userRepository->count([]);
43+
$noUsers = $this->userRepository->noUsers();
4544

46-
if (!$registrationEnabled && 0 !== $userCount) {
45+
// Redirect to the homepage page if registration is disabled, but continue if there are no users yet
46+
if (!$registrationEnabled && !$noUsers) {
4747
return $this->redirectToRoute('dashboard');
4848
}
4949

@@ -54,13 +54,15 @@ public function register(Request $request): Response
5454
$form->handleRequest($request);
5555

5656
if ($form->isSubmitted() && $form->isValid()) {
57-
if (0 === $userCount) {
57+
// The first user gets owner privileges
58+
if ($noUsers) {
5859
$user->setRoles(['ROLE_SUPER_ADMIN', 'ROLE_USER']);
5960
}
6061

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

63-
return $this->redirectToRoute('dashboard_login');
64+
// Automatically authenticate the user after registration
65+
return $security->login($user, 'security.authenticator.form_login.main', 'main');
6466
}
6567

6668
return $this->render('dashboard/security/register.html.twig', [

src/Doctrine/Entity/User.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,27 @@
88
use Doctrine\ORM\Mapping\GeneratedValue;
99
use Doctrine\ORM\Mapping\Id;
1010
use Doctrine\ORM\Mapping\Table;
11+
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
1112
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
1213
use Symfony\Component\Security\Core\User\UserInterface;
1314

1415
#[Entity(repositoryClass: UserRepository::class)]
1516
#[Table(name: '`user`')]
17+
#[UniqueEntity('username', message: 'This username is already taken')]
1618
class User implements UserInterface, PasswordAuthenticatedUserInterface
1719
{
1820
#[Column]
1921
#[GeneratedValue]
2022
#[Id]
2123
private ?int $id = null;
2224

23-
#[Column(length: 80)]
25+
#[Column(length: 80, unique: true)]
2426
private ?string $username = null;
2527

2628
#[Column(length: 180, nullable: true)]
2729
private ?string $name = null;
2830

29-
#[Column(length: 180, unique: true, nullable: true)]
31+
#[Column(length: 180, nullable: true)]
3032
private ?string $email = null;
3133

3234
#[Column]

src/Doctrine/Repository/UserRepository.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@ public function __construct(ManagerRegistry $registry)
2525
parent::__construct($registry, User::class);
2626
}
2727

28+
/**
29+
* Checks if at least one user exists in the database. Used to allow registration for the first user.
30+
*/
31+
public function noUsers(): bool
32+
{
33+
return null === $this->findOneBy([]);
34+
}
35+
2836
public function save(User $entity, bool $flush = false): void
2937
{
3038
$this->getEntityManager()->persist($entity);
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
namespace CodedMonkey\Dirigent\Tests\FunctionalTests\Controller\Dashboard;
4+
5+
use CodedMonkey\Dirigent\Tests\Helper\WebTestCaseTrait;
6+
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
7+
use Symfony\Component\HttpFoundation\Response;
8+
9+
class DashboardSecurityControllerTest extends WebTestCase
10+
{
11+
use WebTestCaseTrait;
12+
13+
/**
14+
* Verify the homepage redirects to the login page when the application is not publicly accessible.
15+
*/
16+
public function testLoginRedirect(): void
17+
{
18+
$client = static::createClient();
19+
$client->request('GET', '/');
20+
21+
$this->assertResponseStatusCodeSame(Response::HTTP_FOUND);
22+
23+
$client->followRedirect();
24+
25+
self::assertSame('/login', $client->getRequest()->getRequestUri());
26+
}
27+
}

translations/validators.en.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@ Enter an email address: Enter an email address
33
Please enter a password: Please enter a password
44
The password fields must match: The password fields must match
55
Your password should be at least {{ limit }} characters: Your password should be at least {{ limit }} characters
6+
7+
This username is already taken: This username is already taken

0 commit comments

Comments
 (0)