Skip to content

Commit 64dbb50

Browse files
committed
add support for multiple login account and switch between them
1 parent 018b3e7 commit 64dbb50

File tree

6 files changed

+281
-2
lines changed

6 files changed

+281
-2
lines changed

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"api-platform/core": "^3.3",
1010
"easycorp/easyadmin-bundle": "^4.10",
1111
"kregel/exception-probe": "^1.0",
12+
"lelyfoto/twig-instanceof": "^1.2",
1213
"symfony/asset": "7.1.*",
1314
"symfony/console": "7.1.*",
1415
"symfony/form": "7.1.*",

src/Repository/RecordPingRepository.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ final class RecordPingRepository extends ServiceEntityRepository implements Reco
2525
{
2626
public function __construct(
2727
ManagerRegistry $registry,
28-
EventDispatcherInterface $dispatcher,
2928
private readonly RecordRepositoryInterface $recordRepository,
3029
) {
3130
parent::__construct($registry, RecordPing::class);
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
<?php
2+
/**
3+
* Created by PhpStorm.
4+
* User: Jozef Môstka
5+
* Date: 9. 10. 2024
6+
* Time: 21:14
7+
*/
8+
9+
namespace BugCatcher\Security;
10+
11+
use Symfony\Bundle\SecurityBundle\Security;
12+
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
13+
use Symfony\Component\DependencyInjection\Attribute\AutowireDecorated;
14+
use Symfony\Component\HttpFoundation\Request;
15+
use Symfony\Component\HttpFoundation\Response;
16+
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
17+
use Symfony\Component\Security\Core\Exception\AuthenticationException;
18+
use Symfony\Component\Security\Http\Authenticator\FormLoginAuthenticator;
19+
use Symfony\Component\Security\Http\Authenticator\InteractiveAuthenticatorInterface;
20+
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
21+
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
22+
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
23+
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
24+
25+
#[AsDecorator('security.authenticator.form_login.main')]
26+
class MultipleLoginAuthenticator implements AuthenticationEntryPointInterface, InteractiveAuthenticatorInterface
27+
{
28+
public function __construct(
29+
#[AutowireDecorated]
30+
private readonly FormLoginAuthenticator $inner,
31+
private readonly Security $security
32+
) {
33+
}
34+
35+
public function start(Request $request, ?AuthenticationException $authException = null): Response
36+
{
37+
return $this->inner->start($request, $authException);
38+
}
39+
40+
public function supports(Request $request): ?bool
41+
{
42+
return ($request->query->has("_switch_user"))
43+
|| $this->inner->supports($request);
44+
}
45+
46+
public function authenticate(Request $request): Passport
47+
{
48+
$currentToken = $this->security->getToken();
49+
if ($currentToken instanceof MultipleLoginToken && $request->query->has("_switch_user")) {
50+
$identifier = $request->query->get("_switch_user");
51+
$userBadge = new UserBadge($identifier, function () use ($currentToken, $identifier) {
52+
foreach ($currentToken->getTokens() as $token) {
53+
if ($token->getUserIdentifier() == $identifier) {
54+
return $token->getUser();
55+
}
56+
}
57+
return null;
58+
});
59+
60+
return new SelfValidatingPassport($userBadge);
61+
}
62+
return $this->inner->authenticate($request);
63+
}
64+
65+
public function createToken(Passport $passport, string $firewallName): TokenInterface
66+
{
67+
$currentToken = $this->security->getToken();
68+
$newToken = $this->inner->createToken($passport, $firewallName);
69+
if ($currentToken === null) {
70+
return $newToken;
71+
}
72+
if ($currentToken instanceof MultipleLoginToken) {
73+
if ($currentToken->trySwitchUser($newToken)) {
74+
return $currentToken;
75+
}
76+
$currentToken->setToken($newToken);
77+
return $currentToken;
78+
}
79+
return new MultipleLoginToken($newToken, $currentToken);
80+
81+
82+
}
83+
84+
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
85+
{
86+
return $this->inner->onAuthenticationSuccess($request, $token, $firewallName);
87+
}
88+
89+
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
90+
{
91+
return $this->inner->onAuthenticationFailure($request, $exception);
92+
}
93+
94+
public function isInteractive(): bool
95+
{
96+
return $this->inner->isInteractive();
97+
}
98+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
/**
3+
* Created by PhpStorm.
4+
* User: Jozef Môstka
5+
* Date: 9. 10. 2024
6+
* Time: 22:11
7+
*/
8+
9+
namespace BugCatcher\Security;
10+
11+
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
12+
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\BadgeInterface;
13+
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
14+
15+
class MultipleLoginBadge extends UserBadge
16+
{
17+
public function __construct(public readonly TokenInterface $token)
18+
{
19+
parent::__construct($token->getUserIdentifier(), function () {
20+
return $this->token->getUser();
21+
});
22+
}
23+
}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
<?php
2+
/**
3+
* Created by PhpStorm.
4+
* User: Jozef Môstka
5+
* Date: 9. 10. 2024
6+
* Time: 21:24
7+
*/
8+
9+
namespace BugCatcher\Security;
10+
11+
use BadMethodCallException;
12+
use Serializable;
13+
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
14+
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
15+
use Symfony\Component\Security\Core\User\UserInterface;
16+
17+
class MultipleLoginToken implements TokenInterface, Serializable
18+
{
19+
20+
/**
21+
* @var TokenInterface[]
22+
*/
23+
private array $tokens = [];
24+
25+
public function __construct(
26+
private TokenInterface $currentToken,
27+
TokenInterface $originalToken
28+
) {
29+
$this->tokens[] = $originalToken;
30+
}
31+
32+
public function setToken(TokenInterface $newToken): void
33+
{
34+
$this->tokens[] = $this->currentToken;
35+
$this->tokens = array_unique($this->tokens, SORT_REGULAR);
36+
$this->currentToken = $newToken;
37+
}
38+
39+
public function trySwitchUser(TokenInterface $newToken): bool
40+
{
41+
foreach ($this->tokens as $token) {
42+
if ($token->getUserIdentifier() == $newToken->getUserIdentifier()) {
43+
$this->tokens[] = $this->currentToken;
44+
$this->tokens = array_unique($this->tokens, SORT_REGULAR);
45+
$this->currentToken = $token;
46+
return true;
47+
}
48+
}
49+
return false;
50+
}
51+
52+
public function getUserIdentifiers(): array
53+
{
54+
$data = [];
55+
foreach ($this->tokens as $token) {
56+
$data[] = $token->getUserIdentifier();
57+
}
58+
return $data;
59+
}
60+
61+
/**
62+
* @return TokenInterface[]
63+
*/
64+
public function getTokens(): array
65+
{
66+
return $this->tokens;
67+
}
68+
69+
public function __serialize(): array
70+
{
71+
$data = $this->tokens;
72+
$data[] = $this->currentToken;
73+
return $data;
74+
}
75+
76+
public function __unserialize(array $data): void
77+
{
78+
$currentToken = array_pop($data);
79+
$this->currentToken = $currentToken;
80+
$this->tokens = $data;
81+
}
82+
83+
public function __toString(): string
84+
{
85+
return $this->currentToken->__toString();
86+
}
87+
88+
public function getUserIdentifier(): string
89+
{
90+
return $this->currentToken->getUserIdentifier();
91+
}
92+
93+
public function getRoleNames(): array
94+
{
95+
return $this->currentToken->getRoleNames();
96+
}
97+
98+
public function getUser(): ?UserInterface
99+
{
100+
return $this->currentToken->getUser();
101+
}
102+
103+
public function setUser(UserInterface $user): void
104+
{
105+
$this->currentToken->setUser($user);
106+
}
107+
108+
public function eraseCredentials(): void
109+
{
110+
$this->currentToken->eraseCredentials();
111+
}
112+
113+
public function getAttributes(): array
114+
{
115+
return $this->currentToken->getAttributes();
116+
}
117+
118+
public function setAttributes(array $attributes): void
119+
{
120+
$this->currentToken->setAttributes($attributes);
121+
}
122+
123+
public function hasAttribute(string $name): bool
124+
{
125+
return $this->currentToken->hasAttribute($name);
126+
}
127+
128+
public function getAttribute(string $name): mixed
129+
{
130+
return $this->currentToken->getAttribute($name);
131+
}
132+
133+
public function setAttribute(string $name, mixed $value): void
134+
{
135+
$this->currentToken->setAttribute($name, $value);
136+
}
137+
138+
public function serialize()
139+
{
140+
throw new BadMethodCallException('Cannot serialize ' . __CLASS__);
141+
}
142+
143+
public function unserialize(string $serialized)
144+
{
145+
$this->__unserialize(unserialize($serialized));
146+
}
147+
148+
149+
}

templates/base.html.twig

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@
4646
</div>
4747
{% endblock %}
4848
<div class="dropup position-fixed bottom-0 end-0 m-5">
49-
<button type="button" class="btn btn-success hide-toggle rounded-circle p-3" data-bs-toggle="dropdown" aria-expanded="false"
49+
<button type="button" class="btn btn-success hide-toggle rounded-circle p-3" data-bs-toggle="dropdown"
50+
aria-expanded="false"
5051
aria-haspopup="true">
5152
<twig:ux:icon name="pajamas:hamburger" width="25" height="25"/>
5253
{# <span class="visually-hidden">Add Category</span> #}
@@ -56,6 +57,14 @@
5657
{% if is_granted('ROLE_ADMIN') %}
5758
<a class="dropdown-item" href="{{ path('bug_catcher.admin') }}">Administration</a>
5859
{% endif %}
60+
{% if app.token is instanceof('\\BugCatcher\\Security\\MultipleLoginToken') %}
61+
<hr class="dropdown-divider">
62+
{% for identifier in app.token.userIdentifiers %}
63+
<a class="dropdown-item"
64+
href="{{ path('bug_catcher.dashboard.index',{"_switch_user":identifier}) }}">{{ identifier }}</a>
65+
{% endfor %}
66+
{% endif %}
67+
5968
<hr class="dropdown-divider">
6069
<a class="dropdown-item" href="{{ path('bug_catcher.dashboard.index',{status:'archived'}) }}">Archived
6170
logs</a>

0 commit comments

Comments
 (0)