2222 * SOFTWARE.
2323 */
2424
25- import { useEffect , useState } from "react" ;
25+ import { useEffect , useRef , useState } from "react" ;
2626import DefaultLayout from "@/layouts/default" ;
2727import { useSecuredApi } from "@/components/auth0" ;
28- import { Auth0ManagementTokenApiResponse , Auth0ManagementTokenResponse , Auth0User , Auth0Permission } from "@/types/data" ;
28+ import { Auth0ManagementTokenApiResponse , Auth0ManagementTokenResponse , Auth0User , Auth0Permission , Tester , GetTestersResponse } from "@/types/data" ;
2929import { useTranslation } from "react-i18next" ;
3030import { Button } from "@heroui/button" ;
3131import { 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
3737export 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