Skip to content

Commit 2440f95

Browse files
committed
Add access URL chooser for users with multiple active portals - refs BT#22639
1 parent e122295 commit 2440f95

File tree

11 files changed

+163
-15
lines changed

11 files changed

+163
-15
lines changed

assets/vue/App.vue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
ref="legacyContainer"
1111
/>
1212
<ConfirmDialog />
13+
14+
<AccessUrlChooser />
1315
</component>
1416
<Toast position="top-center">
1517
<template #message="slotProps">
@@ -56,6 +58,7 @@ import EmptyLayout from "./components/layout/EmptyLayout.vue"
5658
import { useMediaElementLoader } from "./composables/mediaElementLoader"
5759
5860
import apolloClient from "./config/apolloClient"
61+
import AccessUrlChooser from "./components/accessurl/AccessUrlChooser.vue"
5962
6063
provide(DefaultApolloClient, apolloClient)
6164
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<script setup>
2+
import { ref } from "vue"
3+
import { useI18n } from "vue-i18n"
4+
import Dialog from "primevue/dialog"
5+
import { findUserActivePortals } from "../../services/accessurlService"
6+
import { useSecurityStore } from "../../store/securityStore"
7+
import { useNotification } from "../../composables/notification"
8+
import BaseAppLink from "../basecomponents/BaseAppLink.vue"
9+
10+
const securityStore = useSecurityStore()
11+
const { t } = useI18n()
12+
const { showErrorNotification } = useNotification()
13+
14+
const isLoading = ref(true)
15+
const accessUrls = ref([])
16+
17+
if (securityStore.showAccessUrlChooser) {
18+
findUserActivePortals(securityStore.user["@id"])
19+
.then((items) => {
20+
accessUrls.value = items
21+
22+
if (1 === items.length) {
23+
window.location.href = items[0].url
24+
}
25+
})
26+
.catch((error) => showErrorNotification(error))
27+
.finally(() => {
28+
if (1 !== accessUrls.value.length) {
29+
isLoading.value = false
30+
}
31+
})
32+
}
33+
</script>
34+
35+
<template>
36+
<Dialog
37+
v-model:visible="securityStore.showAccessUrlChooser"
38+
:modal="true"
39+
:closable="false"
40+
:header="t('Access URL')"
41+
:style="{ width: '50vw' }"
42+
>
43+
<i
44+
v-if="isLoading"
45+
class="pi pi-spin pi-spinner"
46+
/>
47+
<div
48+
v-else-if="accessUrls.length"
49+
class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4"
50+
>
51+
<div
52+
v-for="accessUrl in accessUrls"
53+
:key="accessUrl.id"
54+
class="text-center"
55+
>
56+
<BaseAppLink :url="accessUrl.url">{{ accessUrl.url }}</BaseAppLink>
57+
<p
58+
v-if="accessUrl.description"
59+
v-text="accessUrl.description"
60+
/>
61+
</div>
62+
</div>
63+
<p
64+
v-else
65+
v-text="t('No active access URLs found')"
66+
/>
67+
</Dialog>
68+
</template>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import baseService from "./baseService"
2+
3+
/**
4+
*
5+
* @param {string} userIri
6+
* @returns {Promise<Object[]>}
7+
*/
8+
export async function findUserActivePortals(userIri) {
9+
const { items } = await baseService.getCollection(`${userIri}/access_urls`)
10+
11+
return items
12+
}

assets/vue/store/securityStore.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ export const useSecurityStore = defineStore("security", () => {
88
const isLoading = ref(true)
99
const isAuthenticated = computed(() => !isEmpty(user.value))
1010

11+
const showAccessUrlChooser = computed(() => {
12+
return !!(isAuthenticated.value && !isAdmin.value && window.is_login_url)
13+
})
14+
1115
/**
1216
* @param {Object} newUserInfo
1317
*/
@@ -87,5 +91,6 @@ export const useSecurityStore = defineStore("security", () => {
8791
isSessionAdmin,
8892
isAdmin,
8993
checkSession,
94+
showAccessUrlChooser,
9095
}
9196
})
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
/* For licensing terms, see /license.txt */
4+
5+
declare(strict_types=1);
6+
7+
namespace Chamilo\CoreBundle\Controller\Api;
8+
9+
use Chamilo\CoreBundle\Entity\User;
10+
use Chamilo\CoreBundle\Repository\Node\AccessUrlRepository;
11+
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
12+
use Symfony\Component\HttpKernel\Attribute\AsController;
13+
14+
#[AsController]
15+
class UserAccessUrlsController extends AbstractController
16+
{
17+
public function __construct(
18+
private readonly AccessUrlRepository $accessUrlRepo
19+
) {}
20+
21+
public function __invoke(User $user): array
22+
{
23+
return $this->accessUrlRepo->getUserActivePortals($user)->getQuery()->getResult();
24+
}
25+
}

src/CoreBundle/Controller/SecurityController.php

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

77
namespace Chamilo\CoreBundle\Controller;
88

9+
use Chamilo\CoreBundle\Entity\AccessUrl;
10+
use Chamilo\CoreBundle\Entity\AccessUrlRelUser;
911
use Chamilo\CoreBundle\Entity\Course;
1012
use Chamilo\CoreBundle\Entity\ExtraFieldValues;
1113
use Chamilo\CoreBundle\Entity\Legal;
1214
use Chamilo\CoreBundle\Entity\User;
1315
use Chamilo\CoreBundle\Helpers\AccessUrlHelper;
1416
use Chamilo\CoreBundle\Helpers\IsAllowedToEditHelper;
1517
use Chamilo\CoreBundle\Helpers\UserHelper;
18+
use Chamilo\CoreBundle\Repository\Node\AccessUrlRepository;
1619
use Chamilo\CoreBundle\Repository\Node\CourseRepository;
1720
use Chamilo\CoreBundle\Repository\TrackELoginRecordRepository;
1821
use Chamilo\CoreBundle\Settings\SettingsManager;
@@ -45,6 +48,7 @@ public function __construct(
4548
private readonly RouterInterface $router,
4649
private readonly AccessUrlHelper $accessUrlHelper,
4750
private readonly IsAllowedToEditHelper $isAllowedToEditHelper,
51+
private readonly AccessUrlRepository $accessUrlRepo,
4852
) {}
4953

5054
#[Route('/login_json', name: 'login_json', methods: ['POST'])]

src/CoreBundle/Entity/AccessUrl.php

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
namespace Chamilo\CoreBundle\Entity;
88

99
use ApiPlatform\Metadata\ApiResource;
10+
use ApiPlatform\Metadata\GetCollection;
11+
use ApiPlatform\Metadata\Link;
12+
use Chamilo\CoreBundle\Controller\Api\UserAccessUrlsController;
1013
use Chamilo\CoreBundle\Repository\Node\AccessUrlRepository;
1114
use DateTime;
1215
use Doctrine\Common\Collections\ArrayCollection;
@@ -31,6 +34,15 @@
3134
#[ORM\Table(name: 'access_url')]
3235
#[Gedmo\Tree(type: 'nested')]
3336
#[ORM\Entity(repositoryClass: AccessUrlRepository::class)]
37+
#[ApiResource(
38+
uriTemplate: '/users/{id}/access_urls',
39+
operations: [new GetCollection(controller: UserAccessUrlsController::class)],
40+
uriVariables: [
41+
'id' => new Link(description: 'User identifier'),
42+
],
43+
normalizationContext: ['groups' => ['user_access_url:read']],
44+
paginationEnabled: false,
45+
)]
3446
class AccessUrl extends AbstractResource implements ResourceInterface, Stringable
3547
{
3648
public const DEFAULT_ACCESS_URL = 'http://localhost/';
@@ -106,10 +118,11 @@ class AccessUrl extends AbstractResource implements ResourceInterface, Stringabl
106118
protected ?AccessUrl $root = null;
107119

108120
#[Assert\NotBlank]
109-
#[Groups(['access_url:read', 'access_url:write'])]
121+
#[Groups(['access_url:read', 'access_url:write', 'user_access_url:read'])]
110122
#[ORM\Column(name: 'url', type: 'string', length: 255)]
111123
protected string $url;
112124

125+
#[Groups(['user_access_url:read'])]
113126
#[ORM\Column(name: 'description', type: 'text')]
114127
protected ?string $description = null;
115128

src/CoreBundle/Entity/User.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1476,6 +1476,9 @@ public function setCurrentUrl(AccessUrl $url): self
14761476
return $this;
14771477
}
14781478

1479+
/**
1480+
* @return Collection<int, AccessUrlRelUser>
1481+
*/
14791482
public function getPortals(): Collection
14801483
{
14811484
return $this->portals;

src/CoreBundle/EventListener/TwigListener.php

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@
66

77
namespace Chamilo\CoreBundle\EventListener;
88

9+
use Chamilo\CoreBundle\Helpers\AccessUrlHelper;
10+
use Chamilo\CoreBundle\Helpers\UserHelper;
911
use Chamilo\CoreBundle\Repository\LanguageRepository;
12+
use Chamilo\CoreBundle\Repository\Node\AccessUrlRepository;
1013
use Symfony\Component\HttpKernel\Event\ControllerEvent;
11-
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
12-
use Symfony\Component\Security\Core\User\UserInterface;
1314
use Symfony\Component\Serializer\SerializerInterface;
1415
use Twig\Environment;
1516

@@ -21,33 +22,32 @@ class TwigListener
2122
public function __construct(
2223
private readonly Environment $twig,
2324
private readonly SerializerInterface $serializer,
24-
private readonly TokenStorageInterface $tokenStorage,
2525
private readonly LanguageRepository $languageRepository,
26+
private readonly UserHelper $userHelper,
27+
private readonly AccessUrlHelper $accessUrlHelper,
2628
) {}
2729

2830
public function __invoke(ControllerEvent $event): void
2931
{
30-
$request = $event->getRequest();
31-
$token = $this->tokenStorage->getToken();
32+
$currentAccessUrl = $this->accessUrlHelper->getCurrent();
33+
$user = $this->userHelper->getCurrent();
3234

3335
$data = null;
3436
$isAuth = false;
35-
if (null !== $token) {
36-
$user = $token->getUser();
37-
if ($user instanceof UserInterface) {
38-
$data = $this->serializer->serialize($user, 'jsonld', [
39-
'groups' => ['user_json:read'],
40-
]);
41-
$isAuth = true;
42-
}
37+
if ($user) {
38+
$data = $this->serializer->serialize($user, 'jsonld', [
39+
'groups' => ['user_json:read'],
40+
]);
41+
$isAuth = true;
4342
}
4443

4544
$languages = $this->languageRepository->getAllAvailable()->getQuery()->getArrayResult();
4645

4746
// $this->twig->addGlobal('text_direction', api_get_text_direction());
4847
$this->twig->addGlobal('is_authenticated', json_encode($isAuth));
4948
$this->twig->addGlobal('user_json', $data ?? json_encode([]));
50-
$this->twig->addGlobal('access_url_id', $request->getSession()->get('access_url_id'));
49+
$this->twig->addGlobal('is_login_url', (int) $currentAccessUrl->isLoginOnly());
50+
$this->twig->addGlobal('access_url_id', $currentAccessUrl->getId());
5151
$this->twig->addGlobal('languages_json', json_encode($languages));
5252
}
5353
}

src/CoreBundle/Repository/Node/AccessUrlRepository.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,18 @@ public function findByUser(User $user): array
5555
->getResult()
5656
;
5757
}
58+
59+
public function getUserActivePortals(User $user): QueryBuilder
60+
{
61+
/** @var QueryBuilder $qb */
62+
$qb = $this->createQueryBuilder('url');
63+
64+
return $qb
65+
->join('url.users', 'users')
66+
->where($qb->expr()->eq('users.user', ':user'))
67+
->andWhere($qb->expr()->eq('url.active', true))
68+
->andWhere($qb->expr()->neq('url.isLoginOnly', true))
69+
->setParameter('user', $user->getId())
70+
;
71+
}
5872
}

0 commit comments

Comments
 (0)