Add forms for profile and password settings; improve tests for settings pages

- Implement `ProfileSettingsForm` and `PasswordChangeForm` components to manage user profile and password updates.
- Add `SessionCard` for session management and related API hooks (`useSession`).
- Update settings page tests to include user state mock and React Query provider for better test reliability.
- Enhance `PasswordSettingsPage` and `ProfileSettingsPage` tests to verify component rendering and user interaction.
- Improve API hook structure with dedicated hooks for session and user profile management.
This commit is contained in:
2025-11-02 23:24:29 +01:00
parent 64a4b3fb11
commit 65f209c679
14 changed files with 1513 additions and 77 deletions

View File

@@ -0,0 +1,177 @@
/**
* Session Management React Query Hooks
*
* Integrates with auto-generated API client for session management.
* All hooks use generated SDK functions for type safety and OpenAPI compliance.
*
* @module lib/api/hooks/useSession
*/
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { listMySessions, revokeSession } from '../client';
import { parseAPIError, getGeneralError } from '../errors';
import type { SessionResponse } from '../generated/types.gen';
// ============================================================================
// Query Keys
// ============================================================================
export const sessionKeys = {
all: ['sessions'] as const,
lists: () => [...sessionKeys.all, 'list'] as const,
list: () => [...sessionKeys.lists()] as const,
};
// ============================================================================
// Types
// ============================================================================
/**
* Session data type (re-export from generated types)
*/
export type Session = SessionResponse;
// ============================================================================
// Queries
// ============================================================================
/**
* Get all active sessions for current user
* GET /api/v1/sessions/me
*
* Returns list of active sessions with device and location info.
*
* @returns React Query result with sessions array
*/
export function useListSessions() {
return useQuery({
queryKey: sessionKeys.list(),
queryFn: async (): Promise<Session[]> => {
const response = await listMySessions({
throwOnError: true,
});
// Extract sessions array from SessionListResponse
return response.data?.sessions || [];
},
staleTime: 30 * 1000, // 30 seconds - sessions change infrequently
retry: 2,
});
}
// ============================================================================
// Mutations
// ============================================================================
/**
* Revoke a specific session
* DELETE /api/v1/sessions/{id}
*
* On success:
* - Removes session from database
* - Invalidates session queries to refetch list
*
* Note: Cannot revoke current session (use logout instead)
*
* @param onSuccess Optional callback after successful revocation
* @returns React Query mutation
*/
export function useRevokeSession(onSuccess?: (message: string) => void) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (sessionId: string) => {
const response = await revokeSession({
path: { session_id: sessionId },
throwOnError: false,
});
if ('error' in response) {
throw response.error;
}
// Type assertion: if no error, response has data
return (response as { data: unknown }).data;
},
onSuccess: (data) => {
// Extract message from response
const message =
typeof data === 'object' &&
data !== null &&
'message' in data &&
typeof (data as Record<string, unknown>).message === 'string'
? (data as { message: string }).message
: 'Session revoked successfully';
// Invalidate sessions list to trigger refetch
queryClient.invalidateQueries({ queryKey: sessionKeys.list() });
// Call custom success callback if provided
if (onSuccess) {
onSuccess(message);
}
},
onError: (error: unknown) => {
const errors = parseAPIError(error);
const generalError = getGeneralError(errors);
console.error('Session revocation failed:', generalError || 'Unknown error');
},
});
}
/**
* Revoke all sessions except the current one
* Convenience hook that revokes multiple sessions
*
* @param onSuccess Optional callback after all sessions revoked
* @returns React Query mutation
*/
export function useRevokeAllOtherSessions(onSuccess?: (message: string) => void) {
const queryClient = useQueryClient();
const { data: sessions } = useListSessions();
return useMutation({
mutationFn: async () => {
if (!sessions) {
throw new Error('No sessions loaded');
}
// Get all non-current sessions
const otherSessions = sessions.filter((s) => !s.is_current);
if (otherSessions.length === 0) {
return { message: 'No other sessions to revoke' };
}
// Revoke all other sessions
const revokePromises = otherSessions.map((session) =>
revokeSession({
path: { session_id: session.id },
throwOnError: false,
})
);
await Promise.all(revokePromises);
return {
message: `Successfully revoked ${otherSessions.length} session${
otherSessions.length > 1 ? 's' : ''
}`,
};
},
onSuccess: (data) => {
// Invalidate sessions list to trigger refetch
queryClient.invalidateQueries({ queryKey: sessionKeys.list() });
// Call custom success callback if provided
if (onSuccess) {
onSuccess(data.message);
}
},
onError: (error: unknown) => {
const errors = parseAPIError(error);
const generalError = getGeneralError(errors);
console.error('Bulk session revocation failed:', generalError || 'Unknown error');
},
});
}

View File

@@ -0,0 +1,83 @@
/**
* User Management React Query Hooks
*
* Integrates with auto-generated API client and authStore for user profile management.
* All hooks use generated SDK functions for type safety and OpenAPI compliance.
*
* @module lib/api/hooks/useUser
*/
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { updateCurrentUser } from '../client';
import { useAuthStore } from '@/lib/stores/authStore';
import type { User } from '@/lib/stores/authStore';
import { parseAPIError, getGeneralError } from '../errors';
import { authKeys } from './useAuth';
// ============================================================================
// Mutations
// ============================================================================
/**
* Update current user profile mutation
* PATCH /api/v1/users/me
*
* On success:
* - Updates user in auth store
* - Invalidates auth queries
*
* @param onSuccess Optional callback after successful update
* @returns React Query mutation
*/
export function useUpdateProfile(onSuccess?: (message: string) => void) {
const queryClient = useQueryClient();
const setUser = useAuthStore((state) => state.setUser);
return useMutation({
mutationFn: async (data: {
first_name?: string;
last_name?: string;
email?: string;
}) => {
const response = await updateCurrentUser({
body: data,
throwOnError: false,
});
if ('error' in response) {
throw response.error;
}
// Type assertion: if no error, response has data
const responseData = (response as { data: unknown }).data;
// Validate response is a user object
if (
typeof responseData !== 'object' ||
responseData === null ||
!('id' in responseData)
) {
throw new Error('Invalid profile update response: missing user data');
}
return responseData as User;
},
onSuccess: (data) => {
// Update auth store with new user data
setUser(data);
// Invalidate auth queries to refetch user data
queryClient.invalidateQueries({ queryKey: authKeys.me });
// Call custom success callback if provided
if (onSuccess) {
onSuccess('Profile updated successfully');
}
},
onError: (error: unknown) => {
const errors = parseAPIError(error);
const generalError = getGeneralError(errors);
console.error('Profile update failed:', generalError || 'Unknown error');
},
});
}