Skip to content

Commit 417a3f7

Browse files
committed
feat: improve users and permissions page
1 parent 6531dfa commit 417a3f7

File tree

8 files changed

+160
-19
lines changed

8 files changed

+160
-19
lines changed

client/src/components/auth0.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import type { Auth0User, Auth0Permission, Auth0Role } from "@/types/data";
3636
export function Profile() {
3737
const { user } = useAuth0();
3838

39-
// console.log(JSON.stringify(user));
39+
4040

4141
return (
4242
<Tooltip content={user?.nickname} delay={750}>
@@ -598,6 +598,7 @@ export const useSecuredApi = () => {
598598
},
599599
},
600600
);
601+
// debug: listAuth0Users called and response status handled above
601602
return (await resp.json()) as Auth0User[];
602603
} catch (error) {
603604
console.error("Failed to list Auth0 users:", error);
@@ -821,13 +822,13 @@ export const userHasPermission = async (
821822
const payload = joseResult.payload as JWTPayload;
822823

823824
if (payload.permissions instanceof Array) {
824-
// console.log(
825+
825826
// `You own this JWT: ${JSON.stringify(payload)}, permission (${permission}) is ${payload.permissions.includes(permission)}`,
826827
// );
827828

828829
return payload.permissions.includes(permission);
829830
} else {
830-
// console.log(
831+
831832
// `The permissions claim is not an array: ${JSON.stringify(
832833
// payload.permissions,
833834
// )}`,

client/src/locales/base/ar-SA.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -269,5 +269,6 @@
269269
"no-management-token": "لا يوجد رمز إدارة",
270270
"confirm-delete-warning": "هل أنت متأكد أنك تريد حذف {{name}}؟",
271271
"failed-loading-user-permissions": "فشل في تحميل أذونات المستخدم",
272-
"error-saving-permissions": "خطأ في حفظ الأذونات"
273-
}
272+
"error-saving-permissions": "خطأ في حفظ الأذونات",
273+
"tester": "اختبار"
274+
}

client/src/locales/base/en-US.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@
8484
"confirm-restore-warning": "Are you sure to restore the database ?",
8585
"tester-name": "Tester Name",
8686
"oauth-ids": "OAuth IDs",
87+
"tester": "Tester",
8788
"no-data-available": "No data available",
8889
"error-fetching-data": "Error fetching data",
8990
"description": "Description",

client/src/locales/base/es-ES.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -269,5 +269,6 @@
269269
"no-management-token": "Sin token de gestión",
270270
"confirm-delete-warning": "¿Estás seguro de que deseas eliminar a {{name}}?",
271271
"failed-loading-user-permissions": "Error al cargar los permisos del usuario",
272-
"error-saving-permissions": "Error al guardar permisos"
273-
}
272+
"error-saving-permissions": "Error al guardar permisos",
273+
"tester": "Ensayador"
274+
}

client/src/locales/base/fr-FR.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -269,5 +269,6 @@
269269
"no-management-token": "Aucun jeton de gestion",
270270
"confirm-delete-warning": "Êtes-vous sûr de vouloir supprimer {{name}} ?",
271271
"failed-loading-user-permissions": "Échec du chargement des autorisations de l'utilisateur",
272-
"error-saving-permissions": "Erreur lors de l'enregistrement des permissions"
273-
}
272+
"error-saving-permissions": "Erreur lors de l'enregistrement des permissions",
273+
"tester": "Testeur"
274+
}

client/src/locales/base/he-IL.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -269,5 +269,6 @@
269269
"no-management-token": "אין אסימון ניהול",
270270
"confirm-delete-warning": "האם אתה בטוח שאתה רוצה למחוק את {{name}}?",
271271
"failed-loading-user-permissions": "נכשל טעינת הרשאות המשתמש",
272-
"error-saving-permissions": "שגיאה בשמירת ההרשאות"
273-
}
272+
"error-saving-permissions": "שגיאה בשמירת ההרשאות",
273+
"tester": "בּוֹחֵן"
274+
}

client/src/locales/base/zh-CN.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -269,5 +269,6 @@
269269
"no-management-token": "没有管理令牌",
270270
"confirm-delete-warning": "您确定要删除 {{name}} 吗?",
271271
"failed-loading-user-permissions": "加载用户权限失败",
272-
"error-saving-permissions": "保存权限时出错"
273-
}
272+
"error-saving-permissions": "保存权限时出错",
273+
"tester": "测试员"
274+
}

client/src/pages/users-and-permissions.tsx

Lines changed: 140 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@
2222
* SOFTWARE.
2323
*/
2424

25-
import { useEffect, useState } from "react";
25+
import { useEffect, useRef, useState } from "react";
2626
import DefaultLayout from "@/layouts/default";
2727
import { useSecuredApi } from "@/components/auth0";
28-
import { Auth0ManagementTokenApiResponse, Auth0ManagementTokenResponse, Auth0User, Auth0Permission } from "@/types/data";
28+
import { Auth0ManagementTokenApiResponse, Auth0ManagementTokenResponse, Auth0User, Auth0Permission, Tester, GetTestersResponse } from "@/types/data";
2929
import { useTranslation } from "react-i18next";
3030
import { Button } from "@heroui/button";
3131
import { Checkbox } from "@heroui/checkbox";
@@ -35,11 +35,15 @@ import ConfirmDeleteModal from "@/components/modals/confirm-delete-modal";
3535
// import { Toast } from "@heroui/toast"; // Not using Toast API directly, using message state
3636

3737
export default function UsersAndPermissionsPage() {
38-
const { getAuth0ManagementToken, listAuth0Users, getUserPermissions, addPermissionToUser, removePermissionFromUser, deleteAuth0User } = useSecuredApi();
38+
const { getAuth0ManagementToken, listAuth0Users, getUserPermissions, addPermissionToUser, removePermissionFromUser, deleteAuth0User, postJson } = useSecuredApi();
39+
const postJsonRef = useRef(postJson);
40+
useEffect(() => { postJsonRef.current = postJson; }, [postJson]);
3941
const [token, setToken] = useState<Auth0ManagementTokenResponse | null>(null);
4042
const { t } = useTranslation();
4143
// replaced message state and inline alert by HeroUI toasts
4244
const [users, setUsers] = useState<Auth0User[]>([]);
45+
const [usersWithTester, setUsersWithTester] = useState<Array<Auth0User & { testerName?: string }>>([]);
46+
const [testerMap, setTesterMap] = useState<Record<string, Tester | undefined>>({});
4347
// roles are not used yet because we work with direct permissions (not roles)
4448
const [editing, setEditing] = useState<Record<string, Record<string, boolean>>>({});
4549
const [selectedUser, setSelectedUser] = useState<Auth0User | null>(null);
@@ -51,14 +55,18 @@ export default function UsersAndPermissionsPage() {
5155
useEffect(() => {
5256
// Fetch Auth0 Management API token for accessing Auth0 management endpoints
5357
getAuth0ManagementToken().then(async (auth0TokenResponse: Auth0ManagementTokenApiResponse) => {
54-
console.log("Token data:", auth0TokenResponse);
5558
setToken(auth0TokenResponse as Auth0ManagementTokenResponse); //TODO verify type correctness (it can be ErrorResponse)
5659
// After token is fetched, list roles and users
5760
if ((auth0TokenResponse as any)?.access_token) {
5861
const mgmtToken = (auth0TokenResponse as any).access_token;
5962
try {
63+
if (!mgmtToken) {
64+
console.error('No mgmtToken available to call listAuth0Users');
65+
}
66+
// Calling listAuth0Users
6067
const u = (await listAuth0Users(mgmtToken)) ?? [];
6168
setUsers(u);
69+
// listAuth0Users resolved
6270
} catch (err) {
6371
console.error('Failed to fetch Auth0 roles or users', err);
6472
}
@@ -68,6 +76,105 @@ export default function UsersAndPermissionsPage() {
6876
});
6977

7078
}, []);
79+
80+
// Ensure the tester map is updated whenever the list of Auth0 users changes
81+
useEffect(() => {
82+
// Reset map when no users
83+
if (!users || users.length === 0) {
84+
setTesterMap({});
85+
return;
86+
}
87+
88+
let cancelled = false;
89+
90+
(async () => {
91+
const ids = Array.from(new Set(users.map((u) => (u.user_id || "").toString().trim()).filter(Boolean)));
92+
// Users effect: found ids
93+
if (ids.length === 0) {
94+
setTesterMap({});
95+
return;
96+
}
97+
98+
try {
99+
const postJsonIsFunction = typeof postJsonRef.current === 'function';
100+
if (!postJsonIsFunction) {
101+
}
102+
const postJsonToUse = postJsonRef.current && typeof postJsonRef.current === 'function' ? postJsonRef.current : postJson;
103+
if (typeof postJsonToUse !== 'function') {
104+
// No postJson available - aborting
105+
setTesterMap({});
106+
return;
107+
}
108+
// Calling POST /testers for ids
109+
const resp = (await postJsonToUse(
110+
`${import.meta.env.API_BASE_URL}/testers`,
111+
{ ids },
112+
)) as GetTestersResponse;
113+
// POST /testers response
114+
if (cancelled) return;
115+
if (resp && resp.success && Array.isArray(resp.data)) {
116+
const map: Record<string, Tester> = {};
117+
for (const tester of resp.data) {
118+
if (Array.isArray(tester.ids)) {
119+
for (const id of tester.ids) {
120+
const key = (id || "").toString().trim();
121+
if (!key) continue;
122+
map[key] = tester;
123+
// Also register the bare id (without provider prefix) to support matches
124+
const bare = key.includes("|") ? key.split("|").pop() : key;
125+
if (bare) map[bare] = tester;
126+
}
127+
}
128+
}
129+
setTesterMap(map);
130+
// Build derived users with testerName to ensure table updates
131+
const derived = users.map((u) => {
132+
const userId = (u.user_id || "").toString().trim();
133+
let testerName = "";
134+
if (userId) {
135+
testerName = map[userId]?.name ?? "";
136+
}
137+
if (!testerName) {
138+
const identities = (u as Auth0User).identities || [];
139+
for (const id of identities) {
140+
const providerKey = `${id.provider}|${id.user_id}`.trim();
141+
if (map[providerKey]) {
142+
testerName = map[providerKey].name;
143+
break;
144+
}
145+
const bare = `${id.user_id}`.trim();
146+
if (map[bare]) {
147+
testerName = map[bare].name;
148+
break;
149+
}
150+
}
151+
}
152+
return { ...u, testerName };
153+
});
154+
setUsersWithTester(derived);
155+
// Derived usersWithTester computed
156+
if (import.meta.env.DEV) {
157+
// Helpful debug info during development
158+
// testerMap built for users
159+
}
160+
} else {
161+
if (import.meta.env.DEV) {
162+
// POST /testers returned no results or success=false
163+
addToast({ title: t('error'), description: t('error-fetching-data'), variant: 'solid', timeout: 3000 });
164+
}
165+
// empty map if not success
166+
setTesterMap({});
167+
}
168+
} catch (err) {
169+
console.error('Failed to build tester map on users change:', err);
170+
setTesterMap({});
171+
}
172+
})();
173+
174+
return () => {
175+
cancelled = true;
176+
};
177+
}, [users]);
71178
const onTogglePermission = (userId: string, permissionName: string) => {
72179
const u = users.find((x) => x.user_id === userId);
73180
if (!u) return;
@@ -123,7 +230,7 @@ export default function UsersAndPermissionsPage() {
123230
}
124231
};
125232

126-
const openUserModal = async (user: any) => {
233+
const openUserModal = async (user: Auth0User) => {
127234
// open modal and fetch permissions for the user only
128235
if (!token) {
129236
addToast({ title: t('error'), description: t('no-management-token'), variant: 'solid', timeout: 5000 });
@@ -195,13 +302,40 @@ export default function UsersAndPermissionsPage() {
195302
<Table aria-label={t('users-and-permissions')} className="my-4">
196303
<TableHeader>
197304
<TableColumn>{t('user')}</TableColumn>
305+
<TableColumn>{t('tester')}</TableColumn>
198306
<TableColumn>{t('email')}</TableColumn>
199307
<TableColumn>{t('actions')}</TableColumn>
200308
</TableHeader>
201-
<TableBody items={users} emptyContent={t('no-data-available')}>
309+
<TableBody items={usersWithTester.length ? usersWithTester : users} emptyContent={t('no-data-available')}>
202310
{(u) => (
203311
<TableRow key={u.user_id}>
204312
<TableCell className="cursor-pointer" onClick={() => openUserModal(u)}>{u.name || u.nickname || u.user_id}</TableCell>
313+
<TableCell>{(() => {
314+
// Use precomputed testerName when available
315+
const precomputed = (u as any).testerName;
316+
if (precomputed) return precomputed;
317+
const userId = (u.user_id || "").toString().trim();
318+
if (!userId) return "";
319+
const tryKey = (k?: string) => {
320+
if (!k) return undefined;
321+
const found = testerMap[k];
322+
return found?.name;
323+
};
324+
// Direct lookup
325+
const direct = tryKey(userId);
326+
if (direct) return direct;
327+
// Check identities fallback
328+
const identities = (u as Auth0User).identities || [];
329+
for (const id of identities) {
330+
const providerKey = `${id.provider}|${id.user_id}`.trim();
331+
const nameFromProvider = tryKey(providerKey);
332+
if (nameFromProvider) return nameFromProvider;
333+
const bare = `${id.user_id}`.trim();
334+
const nameFromBare = tryKey(bare);
335+
if (nameFromBare) return nameFromBare;
336+
}
337+
return "";
338+
})()}</TableCell>
205339
<TableCell>{u.email}</TableCell>
206340
<TableCell>
207341
<Button color="danger" onPress={() => { setConfirmDeleteUser(u); setConfirmDeleteOpen(true); }} disabled={deletingUserId === u.user_id} isLoading={deletingUserId === u.user_id}>{t('delete')}</Button>

0 commit comments

Comments
 (0)