Skip to content

Commit c767dbc

Browse files
committed
New page to assign one auth source to multiple users - refs BT#22986
1 parent da440b5 commit c767dbc

File tree

9 files changed

+274
-9
lines changed

9 files changed

+274
-9
lines changed

assets/vue/router/accessurl.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export default {
2+
meta: { requiresAdmin: true },
3+
path: "/access-url",
4+
component: () => import("../components/layout/SimpleRouterViewLayout.vue"),
5+
children: [
6+
{
7+
path: "auth-sources",
8+
name: "AccessUrlAuthSourcesAssign",
9+
meta: { requiresAuth: true },
10+
component: () => import("../views/accessurl/AccessUrlAuthSourcesAssign.vue"),
11+
},
12+
],
13+
}

assets/vue/router/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import publicPageRoutes from "./publicPage"
1616
import socialNetworkRoutes from "./social"
1717
import fileManagerRoutes from "./filemanager"
1818
import skillRoutes from "./skill"
19+
import accessUrlRoutes from "./accessurl"
1920

2021
//import courseCategoryRoutes from './coursecategory';
2122
import documents from "./documents"
@@ -259,6 +260,7 @@ const router = createRouter({
259260
publicPageRoutes,
260261
skillRoutes,
261262
sessionAdminRoutes,
263+
accessUrlRoutes,
262264
],
263265
})
264266

assets/vue/services/accessurlService.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,9 @@ export async function findUserActivePortals(userIri) {
1010

1111
return items
1212
}
13+
14+
export async function findAll() {
15+
const { items } = await baseService.getCollection("/api/access_urls")
16+
17+
return items
18+
}

assets/vue/services/baseService.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export default {
3333
}
3434

3535
return {
36-
totalItems: data.totalItems,
36+
totalItems: data["hydra:totalItems"] || data.totalItems,
3737
items: data["hydra:member"],
3838
nextPageParams,
3939
}
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
<script setup>
2+
import { ref } from "vue"
3+
import { useI18n } from "vue-i18n"
4+
import { useRouter } from "vue-router"
5+
import SectionHeader from "../../components/layout/SectionHeader.vue"
6+
import BaseButton from "../../components/basecomponents/BaseButton.vue"
7+
import BaseToolbar from "../../components/basecomponents/BaseToolbar.vue"
8+
import BaseSelect from "../../components/basecomponents/BaseSelect.vue"
9+
import BaseTable from "../../components/basecomponents/BaseTable.vue"
10+
import baseService from "../../services/baseService"
11+
import { findAll as listAccessUrl } from "../../services/accessurlService"
12+
import userService from "../../services/userService"
13+
import { useNotification } from "../../composables/notification"
14+
15+
const { t } = useI18n()
16+
const router = useRouter()
17+
18+
const { showErrorNotification, showSuccessNotification } = useNotification()
19+
20+
const accessUrlList = ref([])
21+
const authSourceList = ref([])
22+
const userList = ref([])
23+
const userListTotal = ref(0)
24+
25+
const accessUrl = ref(null)
26+
const authSource = ref(null)
27+
const selectedUsers = ref([])
28+
const isLoadingUserList = ref(true)
29+
const isLoadingAssign = ref(false)
30+
31+
async function listAuthSourcesByAccessUrl({ value: accessUrlIri }) {
32+
authSourceList.value = []
33+
authSource.value = null
34+
35+
try {
36+
const data = await baseService.get("/access-url/auth-sources/list", { access_url: accessUrlIri })
37+
38+
authSourceList.value = data.map((methodName) => ({ label: methodName, value: methodName }))
39+
} catch (error) {
40+
showErrorNotification(error)
41+
}
42+
}
43+
44+
async function listUsers({ page, rows }) {
45+
isLoadingUserList.value = true
46+
47+
try {
48+
const { totalItems, items } = await userService.findAll({
49+
page: page + 1,
50+
itemsPerPage: rows,
51+
})
52+
53+
userListTotal.value = totalItems
54+
userList.value = items
55+
} catch (error) {
56+
showErrorNotification(error)
57+
} finally {
58+
isLoadingUserList.value = false
59+
}
60+
}
61+
62+
async function onPage({ page, rows }) {
63+
await listUsers({ page, rows })
64+
}
65+
66+
async function assignAuthSources() {
67+
isLoadingAssign.value = true
68+
69+
try {
70+
await baseService.post(
71+
"/access-url/auth-sources/assign",
72+
{
73+
users: selectedUsers.value.map((userInfo) => userInfo["@id"]),
74+
auth_source: authSource.value,
75+
access_url: accessUrl.value,
76+
},
77+
true,
78+
)
79+
80+
showSuccessNotification(t("Auth sources assigned successfully"))
81+
82+
selectedUsers.value = []
83+
} catch (e) {
84+
showErrorNotification(e)
85+
} finally {
86+
isLoadingAssign.value = false
87+
}
88+
}
89+
90+
listAccessUrl().then((items) => (accessUrlList.value = items))
91+
listUsers({ page: 0, rows: 20 })
92+
</script>
93+
94+
<template>
95+
<SectionHeader :title="t('Assign auth sources to users')" />
96+
97+
<BaseToolbar>
98+
<template #start>
99+
<BaseButton
100+
:title="t('Back to user assignment page')"
101+
icon="back"
102+
only-icon
103+
type="black"
104+
@click="router.back()"
105+
/>
106+
</template>
107+
</BaseToolbar>
108+
109+
<div class="grid grid-flow-row-dense md:grid-cols-3 gap-4">
110+
<div class="md:col-span-2">
111+
<BaseTable
112+
v-model:selected-items="selectedUsers"
113+
:is-loading="isLoadingUserList"
114+
:total-items="userListTotal"
115+
:values="userList"
116+
data-key="@id"
117+
lazy
118+
@page="onPage"
119+
>
120+
<Column selectionMode="multiple" />
121+
122+
<Column
123+
field="fullName"
124+
:header="t('Full name')"
125+
/>
126+
</BaseTable>
127+
</div>
128+
129+
<div>
130+
<BaseSelect
131+
id="access_url"
132+
v-model="accessUrl"
133+
:disabled="0 === accessUrlList.length"
134+
:label="t('Access URL')"
135+
:options="accessUrlList"
136+
option-label="url"
137+
option-value="@id"
138+
@change="listAuthSourcesByAccessUrl"
139+
/>
140+
141+
<BaseSelect
142+
id="auth_source"
143+
v-model="authSource"
144+
:disabled="0 === authSourceList.length"
145+
:label="t('Auth source')"
146+
:options="authSourceList"
147+
/>
148+
149+
<BaseButton
150+
:disabled="!accessUrl || !authSource || 0 === selectedUsers.length || isLoadingAssign"
151+
:is-loading="isLoadingAssign"
152+
:label="t('Assign')"
153+
icon="save"
154+
type="primary"
155+
@click="assignAuthSources"
156+
/>
157+
</div>
158+
</div>
159+
</template>

public/main/admin/access_urls.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,10 @@
171171
Display::getMdiIcon('file-tree-outline', 'ch-tool-icon', null, ICON_SIZE_MEDIUM, get_lang('Manage course categories')),
172172
api_get_path(WEB_CODE_PATH).'admin/access_url_edit_course_category_to_url.php'
173173
);
174+
$actions .= Display::url(
175+
Display::getMdiIcon('clipboard-account', 'ch-tool-icon', null, ICON_SIZE_MEDIUM, get_lang('Assign auth sources to users')),
176+
"/access-url/auth-sources"
177+
);
174178
}
175179

176180
// 6) If still localhost, show the tooltip inline next to the button

src/CoreBundle/Controller/AccessUrlController.php

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,17 @@
66

77
namespace Chamilo\CoreBundle\Controller;
88

9+
use ApiPlatform\Api\IriConverterInterface;
910
use Chamilo\CoreBundle\Entity\AccessUrl;
1011
use Chamilo\CoreBundle\Entity\User;
12+
use Chamilo\CoreBundle\Helpers\AuthenticationConfigHelper;
1113
use Doctrine\ORM\EntityManagerInterface;
14+
use Exception;
1215
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
16+
use Symfony\Component\HttpFoundation\JsonResponse;
1317
use Symfony\Component\HttpFoundation\Request;
1418
use Symfony\Component\HttpFoundation\Response;
15-
use Symfony\Component\Routing\Annotation\Route;
19+
use Symfony\Component\Routing\Attribute\Route;
1620
use Symfony\Component\Security\Http\Attribute\IsGranted;
1721
use Symfony\Contracts\Translation\TranslatorInterface;
1822

@@ -172,4 +176,75 @@ private function formatReport(string $icon, string $message, array $params): str
172176

173177
return \sprintf('<i class="mdi mdi-%s text-base me-1"></i> %s', $icon, $text);
174178
}
179+
180+
#[Route('/auth-sources/list', methods: ['GET'])]
181+
#[IsGranted('ROLE_ADMIN')]
182+
public function authSourcesList(
183+
Request $request,
184+
AuthenticationConfigHelper $authConfigHelper,
185+
IriConverterInterface $iriConverter,
186+
): JsonResponse {
187+
$accessUrlIri = $request->query->get('access_url', '');
188+
189+
if (!$accessUrlIri) {
190+
throw $this->createNotFoundException('Access URL not found');
191+
}
192+
193+
try {
194+
/** @var AccessUrl $accessUrl */
195+
$accessUrl = $iriConverter->getResourceFromIri($accessUrlIri);
196+
} catch (Exception) {
197+
throw $this->createNotFoundException('Access URL not found');
198+
}
199+
200+
$authSources = $authConfigHelper->getAuthSourceAuthentications($accessUrl);
201+
202+
return new JsonResponse($authSources);
203+
}
204+
205+
/**
206+
* @throws Exception
207+
*/
208+
#[Route('/auth-sources/assign', methods: ['POST'])]
209+
#[IsGranted('ROLE_ADMIN')]
210+
public function authSourcesAssign(
211+
Request $request,
212+
AuthenticationConfigHelper $authConfigHelper,
213+
IriConverterInterface $iriConverter,
214+
EntityManagerInterface $entityManager,
215+
): Response {
216+
$data = json_decode($request->getContent(), true);
217+
218+
if (empty($data['users']) || empty($data['access_url'] || empty($data['auth_source']))) {
219+
throw new Exception('Missing required parameters');
220+
}
221+
222+
try {
223+
/** @var AccessUrl $accessUrl */
224+
$accessUrl = $iriConverter->getResourceFromIri($data['access_url']);
225+
} catch (Exception) {
226+
throw $this->createNotFoundException('Access URL not found');
227+
}
228+
229+
$authSources = $authConfigHelper->getAuthSourceAuthentications($accessUrl);
230+
231+
if (!in_array($data['auth_source'], $authSources)) {
232+
throw new Exception('User authentication method not allowed');
233+
}
234+
235+
foreach ($data['users'] as $userIri) {
236+
try {
237+
/** @var User $user */
238+
$user = $iriConverter->getResourceFromIri($userIri);
239+
} catch (Exception) {
240+
continue;
241+
}
242+
243+
$user->addAuthSourceByAuthentication($data['auth_source'], $accessUrl);
244+
}
245+
246+
$entityManager->flush();
247+
248+
return new Response(null, Response::HTTP_NO_CONTENT);
249+
}
175250
}

src/CoreBundle/Controller/IndexController.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ class IndexController extends BaseController
3535
#[Route('/admin-dashboard/{vueRouting}', name: 'admin_dashboard_vue_entry', requirements: ['vueRouting' => '.+'])]
3636
#[Route('/p/{slug}', name: 'public_page')]
3737
#[Route('/skill/wheel', name: 'skill_wheel')]
38+
#[Route('/access-url/auth-sources', methods: ['GET'])]
3839
public function index(): Response
3940
{
4041
return $this->render('@ChamiloCore/Layout/no_layout.html.twig', ['content' => '']);

src/CoreBundle/Entity/User.php

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2538,12 +2538,16 @@ public function addAuthSource(UserAuthSource $authSource): static
25382538

25392539
public function addAuthSourceByAuthentication(string $authentication, AccessUrl $url): static
25402540
{
2541-
$authSource = (new UserAuthSource())
2542-
->setAuthentication($authentication)
2543-
->setUrl($url)
2544-
;
2541+
$authSource = $this->getAuthSourceByAuthentication($authentication, $url);
2542+
2543+
if (!$authSource) {
2544+
$authSource = (new UserAuthSource())
2545+
->setAuthentication($authentication)
2546+
->setUrl($url)
2547+
;
25452548

2546-
$this->addAuthSource($authSource);
2549+
$this->addAuthSource($authSource);
2550+
}
25472551

25482552
return $this;
25492553
}
@@ -2556,10 +2560,11 @@ public function hasAuthSourceByAuthentication(string $authentication): bool
25562560
);
25572561
}
25582562

2559-
public function getAuthSourceByAuthentication(string $authentication): UserAuthSource
2563+
public function getAuthSourceByAuthentication(string $authentication, AccessUrl $accessUrl): ?UserAuthSource
25602564
{
25612565
return $this->authSources->findFirst(
2562-
fn (UserAuthSource $authSource) => $authSource->getAuthentication() === $authentication
2566+
fn (int $index, UserAuthSource $authSource) => $authSource->getAuthentication() === $authentication
2567+
&& $authSource->getUrl()->getId() === $accessUrl->getId()
25632568
);
25642569
}
25652570

0 commit comments

Comments
 (0)