Merge pull request 'chore: migrate user identity from auth0_sub to UUID' (#219) from issue-206-migrate-user-identity-uuid into main
All checks were successful
Deploy to Staging / Build Images (push) Successful in 6m32s
Deploy to Staging / Deploy to Staging (push) Successful in 23s
Deploy to Staging / Verify Staging (push) Successful in 9s
Deploy to Staging / Notify Staging Ready (push) Successful in 7s
Deploy to Staging / Notify Staging Failure (push) Has been skipped
All checks were successful
Deploy to Staging / Build Images (push) Successful in 6m32s
Deploy to Staging / Deploy to Staging (push) Successful in 23s
Deploy to Staging / Verify Staging (push) Successful in 9s
Deploy to Staging / Notify Staging Ready (push) Successful in 7s
Deploy to Staging / Notify Staging Failure (push) Has been skipped
Reviewed-on: #219
This commit was merged in pull request #219.
This commit is contained in:
@@ -48,7 +48,8 @@ describe('AdminUsersPage', () => {
|
||||
mockUseAdminAccess.mockReturnValue({
|
||||
isAdmin: true,
|
||||
adminRecord: {
|
||||
auth0Sub: 'auth0|123',
|
||||
id: 'admin-uuid-123',
|
||||
userProfileId: 'user-uuid-123',
|
||||
email: 'admin@example.com',
|
||||
role: 'admin',
|
||||
createdAt: '2024-01-01',
|
||||
|
||||
@@ -55,7 +55,8 @@ describe('useAdminAccess', () => {
|
||||
mockAdminApi.verifyAccess.mockResolvedValue({
|
||||
isAdmin: true,
|
||||
adminRecord: {
|
||||
auth0Sub: 'auth0|123',
|
||||
id: 'admin-uuid-123',
|
||||
userProfileId: 'user-uuid-123',
|
||||
email: 'admin@example.com',
|
||||
role: 'admin',
|
||||
createdAt: '2024-01-01',
|
||||
|
||||
@@ -42,7 +42,8 @@ describe('Admin user management hooks', () => {
|
||||
it('should fetch admin users', async () => {
|
||||
const mockAdmins = [
|
||||
{
|
||||
auth0Sub: 'auth0|123',
|
||||
id: 'admin-uuid-123',
|
||||
userProfileId: 'user-uuid-123',
|
||||
email: 'admin1@example.com',
|
||||
role: 'admin',
|
||||
createdAt: '2024-01-01',
|
||||
@@ -68,11 +69,12 @@ describe('Admin user management hooks', () => {
|
||||
describe('useCreateAdmin', () => {
|
||||
it('should create admin and show success toast', async () => {
|
||||
const newAdmin = {
|
||||
auth0Sub: 'auth0|456',
|
||||
id: 'admin-uuid-456',
|
||||
userProfileId: 'user-uuid-456',
|
||||
email: 'newadmin@example.com',
|
||||
role: 'admin',
|
||||
createdAt: '2024-01-01',
|
||||
createdBy: 'auth0|123',
|
||||
createdBy: 'admin-uuid-123',
|
||||
revokedAt: null,
|
||||
updatedAt: '2024-01-01',
|
||||
};
|
||||
@@ -131,11 +133,11 @@ describe('Admin user management hooks', () => {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
result.current.mutate('auth0|123');
|
||||
result.current.mutate('admin-uuid-123');
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(mockAdminApi.revokeAdmin).toHaveBeenCalledWith('auth0|123');
|
||||
expect(mockAdminApi.revokeAdmin).toHaveBeenCalledWith('admin-uuid-123');
|
||||
expect(toast.success).toHaveBeenCalledWith('Admin revoked successfully');
|
||||
});
|
||||
});
|
||||
@@ -148,11 +150,11 @@ describe('Admin user management hooks', () => {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
result.current.mutate('auth0|123');
|
||||
result.current.mutate('admin-uuid-123');
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
|
||||
expect(mockAdminApi.reinstateAdmin).toHaveBeenCalledWith('auth0|123');
|
||||
expect(mockAdminApi.reinstateAdmin).toHaveBeenCalledWith('admin-uuid-123');
|
||||
expect(toast.success).toHaveBeenCalledWith('Admin reinstated successfully');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -101,12 +101,12 @@ export const adminApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
revokeAdmin: async (auth0Sub: string): Promise<void> => {
|
||||
await apiClient.patch(`/admin/admins/${auth0Sub}/revoke`);
|
||||
revokeAdmin: async (id: string): Promise<void> => {
|
||||
await apiClient.patch(`/admin/admins/${id}/revoke`);
|
||||
},
|
||||
|
||||
reinstateAdmin: async (auth0Sub: string): Promise<void> => {
|
||||
await apiClient.patch(`/admin/admins/${auth0Sub}/reinstate`);
|
||||
reinstateAdmin: async (id: string): Promise<void> => {
|
||||
await apiClient.patch(`/admin/admins/${id}/reinstate`);
|
||||
},
|
||||
|
||||
// Audit logs
|
||||
@@ -328,62 +328,62 @@ export const adminApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
get: async (auth0Sub: string): Promise<ManagedUser> => {
|
||||
get: async (userId: string): Promise<ManagedUser> => {
|
||||
const response = await apiClient.get<ManagedUser>(
|
||||
`/admin/users/${encodeURIComponent(auth0Sub)}`
|
||||
`/admin/users/${userId}`
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getVehicles: async (auth0Sub: string): Promise<AdminUserVehiclesResponse> => {
|
||||
getVehicles: async (userId: string): Promise<AdminUserVehiclesResponse> => {
|
||||
const response = await apiClient.get<AdminUserVehiclesResponse>(
|
||||
`/admin/users/${encodeURIComponent(auth0Sub)}/vehicles`
|
||||
`/admin/users/${userId}/vehicles`
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
updateTier: async (auth0Sub: string, data: UpdateUserTierRequest): Promise<ManagedUser> => {
|
||||
updateTier: async (userId: string, data: UpdateUserTierRequest): Promise<ManagedUser> => {
|
||||
const response = await apiClient.patch<ManagedUser>(
|
||||
`/admin/users/${encodeURIComponent(auth0Sub)}/tier`,
|
||||
`/admin/users/${userId}/tier`,
|
||||
data
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
deactivate: async (auth0Sub: string, data?: DeactivateUserRequest): Promise<ManagedUser> => {
|
||||
deactivate: async (userId: string, data?: DeactivateUserRequest): Promise<ManagedUser> => {
|
||||
const response = await apiClient.patch<ManagedUser>(
|
||||
`/admin/users/${encodeURIComponent(auth0Sub)}/deactivate`,
|
||||
`/admin/users/${userId}/deactivate`,
|
||||
data || {}
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
reactivate: async (auth0Sub: string): Promise<ManagedUser> => {
|
||||
reactivate: async (userId: string): Promise<ManagedUser> => {
|
||||
const response = await apiClient.patch<ManagedUser>(
|
||||
`/admin/users/${encodeURIComponent(auth0Sub)}/reactivate`
|
||||
`/admin/users/${userId}/reactivate`
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
updateProfile: async (auth0Sub: string, data: UpdateUserProfileRequest): Promise<ManagedUser> => {
|
||||
updateProfile: async (userId: string, data: UpdateUserProfileRequest): Promise<ManagedUser> => {
|
||||
const response = await apiClient.patch<ManagedUser>(
|
||||
`/admin/users/${encodeURIComponent(auth0Sub)}/profile`,
|
||||
`/admin/users/${userId}/profile`,
|
||||
data
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
promoteToAdmin: async (auth0Sub: string, data?: PromoteToAdminRequest): Promise<AdminUser> => {
|
||||
promoteToAdmin: async (userId: string, data?: PromoteToAdminRequest): Promise<AdminUser> => {
|
||||
const response = await apiClient.patch<AdminUser>(
|
||||
`/admin/users/${encodeURIComponent(auth0Sub)}/promote`,
|
||||
`/admin/users/${userId}/promote`,
|
||||
data || {}
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
hardDelete: async (auth0Sub: string, reason?: string): Promise<{ message: string }> => {
|
||||
hardDelete: async (userId: string, reason?: string): Promise<{ message: string }> => {
|
||||
const response = await apiClient.delete<{ message: string }>(
|
||||
`/admin/users/${encodeURIComponent(auth0Sub)}`,
|
||||
`/admin/users/${userId}`,
|
||||
{ params: reason ? { reason } : undefined }
|
||||
);
|
||||
return response.data;
|
||||
|
||||
@@ -51,7 +51,7 @@ export const useRevokeAdmin = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (auth0Sub: string) => adminApi.revokeAdmin(auth0Sub),
|
||||
mutationFn: (id: string) => adminApi.revokeAdmin(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['admins'] });
|
||||
toast.success('Admin revoked successfully');
|
||||
@@ -66,7 +66,7 @@ export const useReinstateAdmin = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (auth0Sub: string) => adminApi.reinstateAdmin(auth0Sub),
|
||||
mutationFn: (id: string) => adminApi.reinstateAdmin(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['admins'] });
|
||||
toast.success('Admin reinstated successfully');
|
||||
|
||||
@@ -29,8 +29,8 @@ interface ApiError {
|
||||
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,
|
||||
detail: (userId: string) => [...userQueryKeys.all, 'detail', userId] as const,
|
||||
vehicles: (userId: string) => [...userQueryKeys.all, 'vehicles', userId] as const,
|
||||
};
|
||||
|
||||
// Query keys for admin stats
|
||||
@@ -58,13 +58,13 @@ export const useUsers = (params: ListUsersParams = {}) => {
|
||||
/**
|
||||
* Hook to get a single user's details
|
||||
*/
|
||||
export const useUser = (auth0Sub: string) => {
|
||||
export const useUser = (userId: string) => {
|
||||
const { isAuthenticated, isLoading } = useAuth0();
|
||||
|
||||
return useQuery({
|
||||
queryKey: userQueryKeys.detail(auth0Sub),
|
||||
queryFn: () => adminApi.users.get(auth0Sub),
|
||||
enabled: isAuthenticated && !isLoading && !!auth0Sub,
|
||||
queryKey: userQueryKeys.detail(userId),
|
||||
queryFn: () => adminApi.users.get(userId),
|
||||
enabled: isAuthenticated && !isLoading && !!userId,
|
||||
staleTime: 2 * 60 * 1000,
|
||||
gcTime: 5 * 60 * 1000,
|
||||
retry: 1,
|
||||
@@ -78,8 +78,8 @@ export const useUpdateUserTier = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ auth0Sub, data }: { auth0Sub: string; data: UpdateUserTierRequest }) =>
|
||||
adminApi.users.updateTier(auth0Sub, data),
|
||||
mutationFn: ({ userId, data }: { userId: string; data: UpdateUserTierRequest }) =>
|
||||
adminApi.users.updateTier(userId, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: userQueryKeys.all });
|
||||
toast.success('Subscription tier updated');
|
||||
@@ -101,8 +101,8 @@ export const useDeactivateUser = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ auth0Sub, data }: { auth0Sub: string; data?: DeactivateUserRequest }) =>
|
||||
adminApi.users.deactivate(auth0Sub, data),
|
||||
mutationFn: ({ userId, data }: { userId: string; data?: DeactivateUserRequest }) =>
|
||||
adminApi.users.deactivate(userId, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: userQueryKeys.all });
|
||||
toast.success('User deactivated');
|
||||
@@ -124,7 +124,7 @@ export const useReactivateUser = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (auth0Sub: string) => adminApi.users.reactivate(auth0Sub),
|
||||
mutationFn: (userId: string) => adminApi.users.reactivate(userId),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: userQueryKeys.all });
|
||||
toast.success('User reactivated');
|
||||
@@ -146,8 +146,8 @@ export const useUpdateUserProfile = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ auth0Sub, data }: { auth0Sub: string; data: UpdateUserProfileRequest }) =>
|
||||
adminApi.users.updateProfile(auth0Sub, data),
|
||||
mutationFn: ({ userId, data }: { userId: string; data: UpdateUserProfileRequest }) =>
|
||||
adminApi.users.updateProfile(userId, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: userQueryKeys.all });
|
||||
toast.success('User profile updated');
|
||||
@@ -169,8 +169,8 @@ export const usePromoteToAdmin = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ auth0Sub, data }: { auth0Sub: string; data?: PromoteToAdminRequest }) =>
|
||||
adminApi.users.promoteToAdmin(auth0Sub, data),
|
||||
mutationFn: ({ userId, data }: { userId: string; data?: PromoteToAdminRequest }) =>
|
||||
adminApi.users.promoteToAdmin(userId, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: userQueryKeys.all });
|
||||
toast.success('User promoted to admin');
|
||||
@@ -192,8 +192,8 @@ export const useHardDeleteUser = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ auth0Sub, reason }: { auth0Sub: string; reason?: string }) =>
|
||||
adminApi.users.hardDelete(auth0Sub, reason),
|
||||
mutationFn: ({ userId, reason }: { userId: string; reason?: string }) =>
|
||||
adminApi.users.hardDelete(userId, reason),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: userQueryKeys.all });
|
||||
toast.success('User permanently deleted');
|
||||
@@ -228,13 +228,13 @@ export const useAdminStats = () => {
|
||||
/**
|
||||
* Hook to get a user's vehicles (admin view - year, make, model only)
|
||||
*/
|
||||
export const useUserVehicles = (auth0Sub: string) => {
|
||||
export const useUserVehicles = (userId: string) => {
|
||||
const { isAuthenticated, isLoading } = useAuth0();
|
||||
|
||||
return useQuery({
|
||||
queryKey: userQueryKeys.vehicles(auth0Sub),
|
||||
queryFn: () => adminApi.users.getVehicles(auth0Sub),
|
||||
enabled: isAuthenticated && !isLoading && !!auth0Sub,
|
||||
queryKey: userQueryKeys.vehicles(userId),
|
||||
queryFn: () => adminApi.users.getVehicles(userId),
|
||||
enabled: isAuthenticated && !isLoading && !!userId,
|
||||
staleTime: 2 * 60 * 1000,
|
||||
gcTime: 5 * 60 * 1000,
|
||||
retry: 1,
|
||||
|
||||
@@ -104,8 +104,8 @@ const VehicleCountBadge: React.FC<{ count: number; onClick?: () => void }> = ({
|
||||
);
|
||||
|
||||
// Expandable vehicle list component
|
||||
const UserVehiclesList: React.FC<{ auth0Sub: string; isOpen: boolean }> = ({ auth0Sub, isOpen }) => {
|
||||
const { data, isLoading, error } = useUserVehicles(auth0Sub);
|
||||
const UserVehiclesList: React.FC<{ userId: string; isOpen: boolean }> = ({ userId, isOpen }) => {
|
||||
const { data, isLoading, error } = useUserVehicles(userId);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
@@ -215,7 +215,7 @@ export const AdminUsersMobileScreen: React.FC = () => {
|
||||
(newTier: SubscriptionTier) => {
|
||||
if (selectedUser) {
|
||||
updateTierMutation.mutate(
|
||||
{ auth0Sub: selectedUser.auth0Sub, data: { subscriptionTier: newTier } },
|
||||
{ userId: selectedUser.id, data: { subscriptionTier: newTier } },
|
||||
{
|
||||
onSuccess: () => {
|
||||
setShowTierPicker(false);
|
||||
@@ -232,7 +232,7 @@ export const AdminUsersMobileScreen: React.FC = () => {
|
||||
const handleDeactivate = useCallback(() => {
|
||||
if (selectedUser) {
|
||||
deactivateMutation.mutate(
|
||||
{ auth0Sub: selectedUser.auth0Sub, data: { reason: deactivateReason || undefined } },
|
||||
{ userId: selectedUser.id, data: { reason: deactivateReason || undefined } },
|
||||
{
|
||||
onSuccess: () => {
|
||||
setShowDeactivateConfirm(false);
|
||||
@@ -247,7 +247,7 @@ export const AdminUsersMobileScreen: React.FC = () => {
|
||||
|
||||
const handleReactivate = useCallback(() => {
|
||||
if (selectedUser) {
|
||||
reactivateMutation.mutate(selectedUser.auth0Sub, {
|
||||
reactivateMutation.mutate(selectedUser.id, {
|
||||
onSuccess: () => {
|
||||
setShowUserActions(false);
|
||||
setSelectedUser(null);
|
||||
@@ -276,7 +276,7 @@ export const AdminUsersMobileScreen: React.FC = () => {
|
||||
}
|
||||
if (Object.keys(updates).length > 0) {
|
||||
updateProfileMutation.mutate(
|
||||
{ auth0Sub: selectedUser.auth0Sub, data: updates },
|
||||
{ userId: selectedUser.id, data: updates },
|
||||
{
|
||||
onSuccess: () => {
|
||||
setShowEditModal(false);
|
||||
@@ -306,7 +306,7 @@ export const AdminUsersMobileScreen: React.FC = () => {
|
||||
const handlePromoteConfirm = useCallback(() => {
|
||||
if (selectedUser) {
|
||||
promoteToAdminMutation.mutate(
|
||||
{ auth0Sub: selectedUser.auth0Sub, data: { role: promoteRole } },
|
||||
{ userId: selectedUser.id, data: { role: promoteRole } },
|
||||
{
|
||||
onSuccess: () => {
|
||||
setShowPromoteModal(false);
|
||||
@@ -332,7 +332,7 @@ export const AdminUsersMobileScreen: React.FC = () => {
|
||||
const handleHardDeleteConfirm = useCallback(() => {
|
||||
if (selectedUser && hardDeleteConfirmText === 'DELETE') {
|
||||
hardDeleteMutation.mutate(
|
||||
{ auth0Sub: selectedUser.auth0Sub, reason: hardDeleteReason || undefined },
|
||||
{ userId: selectedUser.id, reason: hardDeleteReason || undefined },
|
||||
{
|
||||
onSuccess: () => {
|
||||
setShowHardDeleteModal(false);
|
||||
@@ -509,7 +509,7 @@ export const AdminUsersMobileScreen: React.FC = () => {
|
||||
{users.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
{users.map((user) => (
|
||||
<GlassCard key={user.auth0Sub} padding="md">
|
||||
<GlassCard key={user.id} padding="md">
|
||||
<button
|
||||
onClick={() => handleUserClick(user)}
|
||||
className="w-full text-left min-h-[44px]"
|
||||
@@ -526,7 +526,7 @@ export const AdminUsersMobileScreen: React.FC = () => {
|
||||
<VehicleCountBadge
|
||||
count={user.vehicleCount}
|
||||
onClick={user.vehicleCount > 0 ? () => setExpandedUserId(
|
||||
expandedUserId === user.auth0Sub ? null : user.auth0Sub
|
||||
expandedUserId === user.id ? null : user.id
|
||||
) : undefined}
|
||||
/>
|
||||
<StatusBadge active={!user.deactivatedAt} />
|
||||
@@ -543,8 +543,8 @@ export const AdminUsersMobileScreen: React.FC = () => {
|
||||
</div>
|
||||
</button>
|
||||
<UserVehiclesList
|
||||
auth0Sub={user.auth0Sub}
|
||||
isOpen={expandedUserId === user.auth0Sub}
|
||||
userId={user.id}
|
||||
isOpen={expandedUserId === user.id}
|
||||
/>
|
||||
</GlassCard>
|
||||
))}
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
|
||||
// Admin user types
|
||||
export interface AdminUser {
|
||||
auth0Sub: string;
|
||||
id: string;
|
||||
userProfileId: string;
|
||||
email: string;
|
||||
role: string;
|
||||
createdAt: string;
|
||||
|
||||
Reference in New Issue
Block a user