feat: Add admin vehicle management and profile vehicles display (refs #11)
All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 4m34s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 37s
Deploy to Staging / Verify Staging (pull_request) Successful in 6s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 6s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped

- Add GET /api/admin/stats endpoint for Total Vehicles widget
- Add GET /api/admin/users/:auth0Sub/vehicles endpoint for user vehicle list
- Update AdminUsersPage with Total Vehicles stat and expandable vehicle rows
- Add My Vehicles section to SettingsPage (desktop) and MobileSettingsScreen
- Update AdminUsersMobileScreen with stats header and vehicle expansion
- Add defense-in-depth admin checks and error handling
- Update admin README documentation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Eric Gullickson
2026-01-04 13:18:38 -06:00
parent 2ec208e25a
commit 4fc5b391e1
10 changed files with 639 additions and 67 deletions

View File

@@ -56,6 +56,23 @@ export interface AuditLogsResponse {
total: number;
}
// Admin stats response
export interface AdminStatsResponse {
totalVehicles: number;
totalUsers: number;
}
// User vehicle (admin view)
export interface AdminUserVehicle {
year: number;
make: string;
model: string;
}
export interface AdminUserVehiclesResponse {
vehicles: AdminUserVehicle[];
}
// Admin access verification
export const adminApi = {
// Verify admin access
@@ -64,6 +81,12 @@ export const adminApi = {
return response.data;
},
// Admin dashboard stats
getStats: async (): Promise<AdminStatsResponse> => {
const response = await apiClient.get<AdminStatsResponse>('/admin/stats');
return response.data;
},
// Admin management
listAdmins: async (): Promise<AdminUser[]> => {
const response = await apiClient.get<AdminUser[]>('/admin/admins');
@@ -309,6 +332,13 @@ export const adminApi = {
return response.data;
},
getVehicles: async (auth0Sub: string): Promise<AdminUserVehiclesResponse> => {
const response = await apiClient.get<AdminUserVehiclesResponse>(
`/admin/users/${encodeURIComponent(auth0Sub)}/vehicles`
);
return response.data;
},
updateTier: async (auth0Sub: string, data: UpdateUserTierRequest): Promise<ManagedUser> => {
const response = await apiClient.patch<ManagedUser>(
`/admin/users/${encodeURIComponent(auth0Sub)}/tier`,

View File

@@ -30,6 +30,12 @@ export const userQueryKeys = {
all: ['admin-users'] as const,
list: (params: ListUsersParams) => [...userQueryKeys.all, 'list', params] as const,
detail: (auth0Sub: string) => [...userQueryKeys.all, 'detail', auth0Sub] as const,
vehicles: (auth0Sub: string) => [...userQueryKeys.all, 'vehicles', auth0Sub] as const,
};
// Query keys for admin stats
export const adminStatsQueryKeys = {
all: ['admin-stats'] as const,
};
/**
@@ -201,3 +207,36 @@ export const useHardDeleteUser = () => {
},
});
};
/**
* Hook to get admin dashboard stats (total vehicles, total users)
*/
export const useAdminStats = () => {
const { isAuthenticated, isLoading } = useAuth0();
return useQuery({
queryKey: adminStatsQueryKeys.all,
queryFn: () => adminApi.getStats(),
enabled: isAuthenticated && !isLoading,
staleTime: 2 * 60 * 1000, // 2 minutes
gcTime: 5 * 60 * 1000, // 5 minutes cache time
retry: 1,
refetchOnWindowFocus: false,
});
};
/**
* Hook to get a user's vehicles (admin view - year, make, model only)
*/
export const useUserVehicles = (auth0Sub: string) => {
const { isAuthenticated, isLoading } = useAuth0();
return useQuery({
queryKey: userQueryKeys.vehicles(auth0Sub),
queryFn: () => adminApi.users.getVehicles(auth0Sub),
enabled: isAuthenticated && !isLoading && !!auth0Sub,
staleTime: 2 * 60 * 1000,
gcTime: 5 * 60 * 1000,
retry: 1,
});
};

View File

@@ -16,6 +16,8 @@ import {
useUpdateUserProfile,
usePromoteToAdmin,
useHardDeleteUser,
useAdminStats,
useUserVehicles,
} from '../hooks/useUsers';
import {
ManagedUser,
@@ -82,12 +84,59 @@ const StatusBadge: React.FC<{ active: boolean }> = ({ active }) => (
);
// Vehicle count badge component
const VehicleCountBadge: React.FC<{ count: number }> = ({ count }) => (
<span className="px-2 py-1 rounded-full text-xs font-medium bg-slate-100 text-slate-700">
const VehicleCountBadge: React.FC<{ count: number; onClick?: () => void }> = ({ count, onClick }) => (
<button
onClick={(e) => {
e.stopPropagation();
onClick?.();
}}
disabled={count === 0 || !onClick}
className={`px-2 py-1 rounded-full text-xs font-medium bg-slate-100 text-slate-700 flex items-center gap-1 ${count > 0 && onClick ? 'cursor-pointer' : ''}`}
>
{count} {count === 1 ? 'vehicle' : 'vehicles'}
</span>
{count > 0 && onClick && (
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
)}
</button>
);
// Expandable vehicle list component
const UserVehiclesList: React.FC<{ auth0Sub: string; isOpen: boolean }> = ({ auth0Sub, isOpen }) => {
const { data, isLoading, error } = useUserVehicles(auth0Sub);
if (!isOpen) return null;
return (
<div className="mt-3 pt-3 border-t border-slate-200 dark:border-silverstone">
<p className="text-xs font-semibold text-slate-500 dark:text-silverstone uppercase mb-2 flex items-center gap-1">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 4H5c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h4l3 3 3-3h4c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zM8 11V9h8v2m-8 4v-2h5v2" />
</svg>
Vehicles
</p>
{isLoading ? (
<div className="flex justify-center py-2">
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-blue-600" />
</div>
) : error ? (
<p className="text-xs text-red-500">Failed to load vehicles</p>
) : !data?.vehicles?.length ? (
<p className="text-xs text-slate-400">No vehicles registered</p>
) : (
<div className="space-y-1">
{data.vehicles.map((vehicle, idx) => (
<div key={idx} className="text-sm text-slate-600 dark:text-silverstone bg-slate-50 dark:bg-carbon px-2 py-1 rounded">
{vehicle.year} {vehicle.make} {vehicle.model}
</div>
))}
</div>
)}
</div>
);
};
export const AdminUsersMobileScreen: React.FC = () => {
const { isAdmin, loading: adminLoading } = useAdminAccess();
@@ -105,6 +154,12 @@ export const AdminUsersMobileScreen: React.FC = () => {
// Query
const { data, isLoading, error, refetch } = useUsers(params);
// Admin stats
const { data: statsData } = useAdminStats();
// Expanded user for vehicle list
const [expandedUserId, setExpandedUserId] = useState<string | null>(null);
// Mutations
const updateTierMutation = useUpdateUserTier();
const deactivateMutation = useDeactivateUser();
@@ -328,10 +383,17 @@ export const AdminUsersMobileScreen: React.FC = () => {
<div className="space-y-4 pb-20 p-4">
{/* Header */}
<div className="text-center mb-6">
<h1 className="text-2xl font-bold text-slate-800">User Management</h1>
<p className="text-slate-500 mt-2">
{total} user{total !== 1 ? 's' : ''}
</p>
<h1 className="text-2xl font-bold text-slate-800 dark:text-avus">User Management</h1>
<div className="flex justify-center gap-4 mt-3">
<div className="bg-blue-50 dark:bg-blue-900/30 px-4 py-2 rounded-lg">
<p className="text-xl font-bold text-blue-700 dark:text-blue-400">{statsData?.totalUsers ?? total}</p>
<p className="text-xs text-blue-600 dark:text-blue-300">Users</p>
</div>
<div className="bg-green-50 dark:bg-green-900/30 px-4 py-2 rounded-lg">
<p className="text-xl font-bold text-green-700 dark:text-green-400">{statsData?.totalVehicles ?? 0}</p>
<p className="text-xs text-green-600 dark:text-green-300">Vehicles</p>
</div>
</div>
</div>
{/* Search Bar */}
@@ -454,13 +516,18 @@ export const AdminUsersMobileScreen: React.FC = () => {
>
<div className="flex items-start justify-between">
<div className="flex-1 min-w-0">
<p className="font-medium text-slate-800 truncate">{user.email}</p>
<p className="font-medium text-slate-800 dark:text-avus truncate">{user.email}</p>
{user.displayName && (
<p className="text-sm text-slate-500 truncate">{user.displayName}</p>
<p className="text-sm text-slate-500 dark:text-silverstone truncate">{user.displayName}</p>
)}
<div className="flex flex-wrap gap-2 mt-2">
<TierBadge tier={user.subscriptionTier} />
<VehicleCountBadge count={user.vehicleCount} />
<VehicleCountBadge
count={user.vehicleCount}
onClick={user.vehicleCount > 0 ? () => setExpandedUserId(
expandedUserId === user.auth0Sub ? null : user.auth0Sub
) : undefined}
/>
<StatusBadge active={!user.deactivatedAt} />
{user.isAdmin && (
<span className="px-2 py-1 rounded-full text-xs font-medium bg-amber-100 text-amber-700">
@@ -474,6 +541,10 @@ export const AdminUsersMobileScreen: React.FC = () => {
</svg>
</div>
</button>
<UserVehiclesList
auth0Sub={user.auth0Sub}
isOpen={expandedUserId === user.auth0Sub}
/>
</GlassCard>
))}

View File

@@ -5,6 +5,7 @@ import { MobileContainer } from '../../../shared-minimal/components/mobile/Mobil
import { useSettings } from '../hooks/useSettings';
import { useProfile, useUpdateProfile } from '../hooks/useProfile';
import { useExportUserData } from '../hooks/useExportUserData';
import { useVehicles } from '../../vehicles/hooks/useVehicles';
import { useAdminAccess } from '../../../core/auth/useAdminAccess';
import { useNavigationStore } from '../../../core/store';
import { DeleteAccountModal } from './DeleteAccountModal';
@@ -80,6 +81,7 @@ export const MobileSettingsScreen: React.FC = () => {
const { data: profile, isLoading: profileLoading } = useProfile();
const updateProfileMutation = useUpdateProfile();
const exportMutation = useExportUserData();
const { data: vehicles, isLoading: vehiclesLoading } = useVehicles();
const { isAdmin, loading: adminLoading } = useAdminAccess();
const [showDataExport, setShowDataExport] = useState(false);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
@@ -316,6 +318,58 @@ export const MobileSettingsScreen: React.FC = () => {
</div>
</GlassCard>
{/* My Vehicles Section */}
<GlassCard padding="md">
<div>
<div className="flex justify-between items-center mb-4">
<div className="flex items-center gap-2">
<svg className="w-5 h-5 text-primary-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 4H5c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h4l3 3 3-3h4c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zM8 11V9h8v2m-8 4v-2h5v2" />
</svg>
<h2 className="text-lg font-semibold text-slate-800 dark:text-avus">
My Vehicles
</h2>
{!vehiclesLoading && vehicles && (
<span className="text-sm text-slate-500 dark:text-titanio">({vehicles.length})</span>
)}
</div>
<button
onClick={() => navigateToScreen('Vehicles')}
className="px-3 py-1.5 bg-primary-500 text-white rounded-lg text-sm font-medium hover:bg-primary-600 transition-colors dark:bg-primary-600 dark:hover:bg-primary-700"
style={{ minHeight: '44px', minWidth: '44px' }}
>
Manage
</button>
</div>
{vehiclesLoading ? (
<div className="flex justify-center py-4">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-primary-500"></div>
</div>
) : !vehicles?.length ? (
<p className="text-sm text-slate-500 dark:text-titanio py-2">
No vehicles registered. Add your first vehicle to get started.
</p>
) : (
<div className="space-y-2">
{vehicles.map((vehicle) => (
<div
key={vehicle.id}
className="p-3 bg-slate-50 dark:bg-nero rounded-lg"
>
<p className="font-medium text-slate-800 dark:text-avus">
{vehicle.year} {vehicle.make} {vehicle.model}
</p>
{vehicle.nickname && (
<p className="text-sm text-slate-500 dark:text-titanio">{vehicle.nickname}</p>
)}
</div>
))}
</div>
)}
</div>
</GlassCard>
{/* Notifications Section */}
<GlassCard padding="md">
<div>

View File

@@ -9,6 +9,7 @@ import { useUnits } from '../core/units/UnitsContext';
import { useAdminAccess } from '../core/auth/useAdminAccess';
import { useProfile, useUpdateProfile } from '../features/settings/hooks/useProfile';
import { useExportUserData } from '../features/settings/hooks/useExportUserData';
import { useVehicles } from '../features/vehicles/hooks/useVehicles';
import { useTheme } from '../shared-minimal/theme/ThemeContext';
import { DeleteAccountDialog } from '../features/settings/components/DeleteAccountDialog';
import { PendingDeletionBanner } from '../features/settings/components/PendingDeletionBanner';
@@ -36,6 +37,7 @@ import PaletteIcon from '@mui/icons-material/Palette';
import SecurityIcon from '@mui/icons-material/Security';
import StorageIcon from '@mui/icons-material/Storage';
import AdminPanelSettingsIcon from '@mui/icons-material/AdminPanelSettings';
import DirectionsCarIcon from '@mui/icons-material/DirectionsCar';
import EditIcon from '@mui/icons-material/Edit';
import SaveIcon from '@mui/icons-material/Save';
import CancelIcon from '@mui/icons-material/Cancel';
@@ -53,6 +55,9 @@ export const SettingsPage: React.FC = () => {
// Profile state
const { data: profile, isLoading: profileLoading } = useProfile();
const updateProfileMutation = useUpdateProfile();
// Vehicles state (for My Vehicles section)
const { data: vehicles, isLoading: vehiclesLoading } = useVehicles();
const [isEditingProfile, setIsEditingProfile] = useState(false);
const [editedDisplayName, setEditedDisplayName] = useState('');
const [editedNotificationEmail, setEditedNotificationEmail] = useState('');
@@ -277,6 +282,62 @@ export const SettingsPage: React.FC = () => {
)}
</Card>
{/* My Vehicles Section */}
<Card>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<DirectionsCarIcon color="primary" />
<Typography variant="h6" sx={{ fontWeight: 600 }}>
My Vehicles
</Typography>
{!vehiclesLoading && vehicles && (
<Typography variant="body2" color="text.secondary">
({vehicles.length})
</Typography>
)}
</Box>
<MuiButton
variant="contained"
size="small"
onClick={() => navigate('/garage')}
sx={{
backgroundColor: 'primary.main',
color: 'primary.contrastText',
'&:hover': {
backgroundColor: 'primary.dark'
}
}}
>
Manage
</MuiButton>
</Box>
{vehiclesLoading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 3 }}>
<CircularProgress size={24} />
</Box>
) : !vehicles?.length ? (
<Typography variant="body2" color="text.secondary" sx={{ py: 2 }}>
No vehicles registered. Add your first vehicle to get started.
</Typography>
) : (
<List disablePadding>
{vehicles.map((vehicle, index) => (
<React.Fragment key={vehicle.id}>
{index > 0 && <Divider />}
<ListItem sx={{ py: 1.5 }}>
<ListItemText
primary={`${vehicle.year} ${vehicle.make} ${vehicle.model}`}
secondary={vehicle.nickname || undefined}
primaryTypographyProps={{ fontWeight: 500 }}
/>
</ListItem>
</React.Fragment>
))}
</List>
)}
</Card>
{/* Notifications Section */}
<Card>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 3 }}>

View File

@@ -10,6 +10,7 @@ import {
Button,
Chip,
CircularProgress,
Collapse,
Dialog,
DialogActions,
DialogContent,
@@ -44,6 +45,9 @@ import {
Edit,
Security,
DeleteForever,
DirectionsCar,
KeyboardArrowDown,
KeyboardArrowUp,
} from '@mui/icons-material';
import { useAdminAccess } from '../../core/auth/useAdminAccess';
import {
@@ -54,6 +58,8 @@ import {
useUpdateUserProfile,
usePromoteToAdmin,
useHardDeleteUser,
useAdminStats,
useUserVehicles,
} from '../../features/admin/hooks/useUsers';
import {
ManagedUser,
@@ -64,6 +70,58 @@ import { AdminSectionHeader } from '../../features/admin/components';
const PAGE_SIZE_OPTIONS = [10, 20, 50, 100];
// Expandable vehicle row component
const UserVehiclesRow: React.FC<{ auth0Sub: string; isOpen: boolean }> = ({ auth0Sub, isOpen }) => {
const { data, isLoading, error } = useUserVehicles(auth0Sub);
if (!isOpen) return null;
return (
<TableRow>
<TableCell colSpan={9} sx={{ py: 0, bgcolor: 'action.hover' }}>
<Collapse in={isOpen} timeout="auto" unmountOnExit>
<Box sx={{ py: 2, px: 3 }}>
<Typography variant="subtitle2" sx={{ mb: 1, display: 'flex', alignItems: 'center', gap: 1 }}>
<DirectionsCar fontSize="small" />
Vehicles
</Typography>
{isLoading ? (
<CircularProgress size={20} />
) : error ? (
<Typography variant="body2" color="error">
Failed to load vehicles
</Typography>
) : !data?.vehicles?.length ? (
<Typography variant="body2" color="text.secondary">
No vehicles registered
</Typography>
) : (
<Table size="small" sx={{ maxWidth: 500 }}>
<TableHead>
<TableRow>
<TableCell sx={{ fontWeight: 600 }}>Year</TableCell>
<TableCell sx={{ fontWeight: 600 }}>Make</TableCell>
<TableCell sx={{ fontWeight: 600 }}>Model</TableCell>
</TableRow>
</TableHead>
<TableBody>
{data.vehicles.map((vehicle, idx) => (
<TableRow key={idx}>
<TableCell>{vehicle.year}</TableCell>
<TableCell>{vehicle.make}</TableCell>
<TableCell>{vehicle.model}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</Box>
</Collapse>
</TableCell>
</TableRow>
);
};
export const AdminUsersPage: React.FC = () => {
const { isAdmin, loading: adminLoading } = useAdminAccess();
@@ -110,6 +168,12 @@ export const AdminUsersPage: React.FC = () => {
const [hardDeleteReason, setHardDeleteReason] = useState('');
const [hardDeleteConfirmText, setHardDeleteConfirmText] = useState('');
// Expanded row state for vehicle list
const [expandedRow, setExpandedRow] = useState<string | null>(null);
// Admin stats query
const { data: statsData } = useAdminStats();
// Handlers
const handleSearch = useCallback(() => {
setParams(prev => ({ ...prev, search: searchInput || undefined, page: 1 }));
@@ -319,7 +383,10 @@ export const AdminUsersPage: React.FC = () => {
<Box sx={{ py: 2, display: 'flex', flexDirection: 'column', gap: 3 }}>
<AdminSectionHeader
title="User Management"
stats={[{ label: 'Total Users', value: total }]}
stats={[
{ label: 'Total Users', value: statsData?.totalUsers ?? total },
{ label: 'Total Vehicles', value: statsData?.totalVehicles ?? 0 },
]}
/>
{/* Filters Bar */}
@@ -429,55 +496,83 @@ export const AdminUsersPage: React.FC = () => {
</TableHead>
<TableBody>
{users.map((user) => (
<TableRow
key={user.auth0Sub}
sx={{ opacity: user.deactivatedAt ? 0.6 : 1 }}
>
<TableCell>{user.email}</TableCell>
<TableCell>{user.displayName || '-'}</TableCell>
<TableCell>
<FormControl size="small" sx={{ minWidth: 100 }}>
<Select
value={user.subscriptionTier}
onChange={(e) =>
handleTierChange(user.auth0Sub, e.target.value as SubscriptionTier)
}
disabled={!!user.deactivatedAt || updateTierMutation.isPending}
<React.Fragment key={user.auth0Sub}>
<TableRow
sx={{
opacity: user.deactivatedAt ? 0.6 : 1,
'& > *': { borderBottom: expandedRow === user.auth0Sub ? 'unset' : undefined },
}}
>
<TableCell>{user.email}</TableCell>
<TableCell>{user.displayName || '-'}</TableCell>
<TableCell>
<FormControl size="small" sx={{ minWidth: 100 }}>
<Select
value={user.subscriptionTier}
onChange={(e) =>
handleTierChange(user.auth0Sub, e.target.value as SubscriptionTier)
}
disabled={!!user.deactivatedAt || updateTierMutation.isPending}
size="small"
>
<MenuItem value="free">Free</MenuItem>
<MenuItem value="pro">Pro</MenuItem>
<MenuItem value="enterprise">Enterprise</MenuItem>
</Select>
</FormControl>
</TableCell>
<TableCell>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
{user.vehicleCount > 0 && (
<IconButton
size="small"
onClick={() => setExpandedRow(
expandedRow === user.auth0Sub ? null : user.auth0Sub
)}
aria-label="show vehicles"
sx={{ minWidth: 44, minHeight: 44 }}
>
{expandedRow === user.auth0Sub ? (
<KeyboardArrowUp fontSize="small" />
) : (
<KeyboardArrowDown fontSize="small" />
)}
</IconButton>
)}
<Typography variant="body2">{user.vehicleCount}</Typography>
</Box>
</TableCell>
<TableCell>
<Chip
label={user.deactivatedAt ? 'Deactivated' : 'Active'}
color={user.deactivatedAt ? 'error' : 'success'}
size="small"
/>
</TableCell>
<TableCell>
{user.isAdmin && (
<Tooltip title={`Admin (${user.adminRole})`}>
<AdminPanelSettings color="primary" />
</Tooltip>
)}
</TableCell>
<TableCell>
{new Date(user.createdAt).toLocaleDateString()}
</TableCell>
<TableCell align="right">
<IconButton
size="small"
onClick={(e) => handleMenuOpen(e, user)}
>
<MenuItem value="free">Free</MenuItem>
<MenuItem value="pro">Pro</MenuItem>
<MenuItem value="enterprise">Enterprise</MenuItem>
</Select>
</FormControl>
</TableCell>
<TableCell>{user.vehicleCount}</TableCell>
<TableCell>
<Chip
label={user.deactivatedAt ? 'Deactivated' : 'Active'}
color={user.deactivatedAt ? 'error' : 'success'}
size="small"
/>
</TableCell>
<TableCell>
{user.isAdmin && (
<Tooltip title={`Admin (${user.adminRole})`}>
<AdminPanelSettings color="primary" />
</Tooltip>
)}
</TableCell>
<TableCell>
{new Date(user.createdAt).toLocaleDateString()}
</TableCell>
<TableCell align="right">
<IconButton
size="small"
onClick={(e) => handleMenuOpen(e, user)}
>
<MoreVert />
</IconButton>
</TableCell>
</TableRow>
<MoreVert />
</IconButton>
</TableCell>
</TableRow>
<UserVehiclesRow
auth0Sub={user.auth0Sub}
isOpen={expandedRow === user.auth0Sub}
/>
</React.Fragment>
))}
</TableBody>
</Table>