KrakenHashes Frontend Development Guide¶
This guide provides comprehensive documentation for developing the KrakenHashes frontend application. The frontend is built with React, TypeScript, Material-UI, and React Query.
Table of Contents¶
- Development Environment Setup
- Project Structure and Organization
- Component Development Patterns
- State Management with React Query
- API Integration and Services
- Material-UI Theming and Styling
- TypeScript Conventions
- Testing Approaches
Development Environment Setup¶
Prerequisites¶
- Node.js 18+ and npm 9+
- Docker and Docker Compose (for backend services)
- A modern web browser with developer tools
Initial Setup¶
-
Clone the repository:
-
Install dependencies:
-
Configure environment variables: Create a
.env.local
file in the frontend directory: -
Start the development server:
The application will be available athttp://localhost:3000
Running with Backend Services¶
For full functionality, run the backend services using Docker:
Development Scripts¶
npm start # Start development server (port 3000)
npm run build # Production build
npm test # Run tests
npm run eject # Eject from Create React App (use with caution)
Project Structure and Organization¶
The frontend follows a feature-based organization pattern:
frontend/src/
├── api/ # API version checking
│ └── version.ts
├── components/ # Reusable components
│ ├── admin/ # Admin-specific components
│ ├── agent/ # Agent management components
│ ├── auth/ # Authentication components
│ ├── common/ # Shared common components
│ ├── hashlist/ # Hashlist management components
│ ├── pot/ # Cracked passwords components
│ └── settings/ # Settings components
├── contexts/ # React contexts
│ └── AuthContext.tsx # Authentication context provider
├── hooks/ # Custom React hooks
│ ├── useAuth.tsx # Authentication hook
│ ├── useConfirm.tsx # Confirmation dialog hook
│ ├── useDebounce.ts # Debounce hook
│ └── useVouchers.ts # Voucher management hook
├── pages/ # Page components (routes)
│ ├── admin/ # Admin pages
│ ├── settings/ # Settings pages
│ └── ... # Other page components
├── services/ # API service layer
│ ├── api.ts # Axios configuration
│ ├── auth.ts # Authentication services
│ └── ... # Domain-specific services
├── styles/ # Global styles
│ └── theme.ts # Material-UI theme
├── types/ # TypeScript type definitions
│ ├── agent.ts # Agent types
│ ├── auth.ts # Authentication types
│ └── ... # Domain-specific types
├── utils/ # Utility functions
│ ├── formatters.ts # Data formatting utilities
│ ├── validation.ts # Validation utilities
│ └── ... # Other utilities
├── App.tsx # Root application component
├── config.ts # Application configuration
└── index.tsx # Application entry point
Component Development Patterns¶
Component Structure¶
Components follow a consistent structure with TypeScript interfaces and comprehensive documentation:
/**
* ComponentName - Brief description of the component
*
* Features:
* - Key feature 1
* - Key feature 2
*
* Dependencies:
* - External libraries used
* - Internal components/hooks
*
* Error Scenarios:
* - Possible error conditions
* - How they're handled
*/
import React, { useState, useCallback } from 'react';
import { Box, Typography } from '@mui/material';
interface ComponentNameProps {
title: string;
onAction?: () => void;
}
const ComponentName: React.FC<ComponentNameProps> = ({ title, onAction }) => {
const [state, setState] = useState<string>('');
const handleAction = useCallback(() => {
// Handle action
onAction?.();
}, [onAction]);
return (
<Box>
<Typography variant="h5">{title}</Typography>
{/* Component content */}
</Box>
);
};
export default ComponentName;
Layout Components¶
The application uses a main Layout component with navigation:
// components/Layout.tsx
const Layout: React.FC = () => {
const [open, setOpen] = useState<boolean>(true);
const navigate = useNavigate();
const { userRole } = useAuth();
return (
<Box sx={{ display: 'flex', minHeight: '100vh' }}>
<AppBar position="fixed">
<Toolbar>
<Typography variant="h6">KrakenHashes</Typography>
<UserMenu />
</Toolbar>
</AppBar>
<Drawer variant="permanent" open={open}>
<List>
{menuItems.map((item) => (
<ListItem button key={item.text} onClick={() => navigate(item.path)}>
<ListItemIcon>{item.icon}</ListItemIcon>
<ListItemText primary={item.text} />
</ListItem>
))}
</List>
{userRole === 'admin' && <AdminMenu />}
</Drawer>
<Box component="main" sx={{ flexGrow: 1, p: 3 }}>
<Outlet />
</Box>
</Box>
);
};
Form Components¶
Forms use React Hook Form with Material-UI integration:
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';
const schema = z.object({
name: z.string().min(1, 'Name is required'),
email: z.string().email('Invalid email'),
});
type FormData = z.infer<typeof schema>;
const FormComponent: React.FC = () => {
const { register, handleSubmit, formState: { errors } } = useForm<FormData>({
resolver: zodResolver(schema),
});
const onSubmit = (data: FormData) => {
// Handle form submission
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<TextField
{...register('name')}
error={!!errors.name}
helperText={errors.name?.message}
label="Name"
fullWidth
/>
<Button type="submit" variant="contained">
Submit
</Button>
</form>
);
};
State Management with React Query¶
Query Client Configuration¶
The application uses React Query for server state management:
// App.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 minutes
refetchOnWindowFocus: true,
},
},
});
function App() {
return (
<QueryClientProvider client={queryClient}>
{/* Application components */}
</QueryClientProvider>
);
}
Using Queries¶
Example of fetching data with React Query:
import { useQuery } from '@tanstack/react-query';
import { getWordlists } from '../services/wordlists';
const WordlistsComponent: React.FC = () => {
const { data, isLoading, error, refetch } = useQuery({
queryKey: ['wordlists'],
queryFn: getWordlists,
});
if (isLoading) return <CircularProgress />;
if (error) return <Alert severity="error">Failed to load wordlists</Alert>;
return (
<Box>
{data?.map((wordlist) => (
<WordlistItem key={wordlist.id} wordlist={wordlist} />
))}
</Box>
);
};
Using Mutations¶
Example of mutating data with React Query:
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { updateWordlist } from '../services/wordlists';
const EditWordlistDialog: React.FC<{ wordlist: Wordlist }> = ({ wordlist }) => {
const queryClient = useQueryClient();
const { enqueueSnackbar } = useSnackbar();
const mutation = useMutation({
mutationFn: (data: UpdateWordlistData) => updateWordlist(wordlist.id, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['wordlists'] });
enqueueSnackbar('Wordlist updated successfully', { variant: 'success' });
},
onError: (error) => {
enqueueSnackbar('Failed to update wordlist', { variant: 'error' });
},
});
const handleSubmit = (data: UpdateWordlistData) => {
mutation.mutate(data);
};
return (
<Dialog open onClose={onClose}>
<DialogContent>
{/* Form fields */}
</DialogContent>
<DialogActions>
<Button onClick={() => handleSubmit(formData)} disabled={mutation.isPending}>
Save
</Button>
</DialogActions>
</Dialog>
);
};
API Integration and Services¶
Axios Configuration¶
The API service layer uses Axios with interceptors:
// services/api.ts
import axios from 'axios';
const API_URL = process.env.REACT_APP_API_URL || 'https://localhost:31337';
export const api = axios.create({
baseURL: API_URL,
withCredentials: true, // Required for cookies/session
});
// Request interceptor
api.interceptors.request.use((config) => {
// Log requests in development
if (process.env.NODE_ENV === 'development') {
console.debug(`[API] ${config.method?.toUpperCase()} ${config.url}`, config.data);
}
return config;
});
// Response interceptor
api.interceptors.response.use(
(response) => response,
async (error) => {
if (error.response?.status === 401) {
// Handle authentication errors
window.location.href = '/login';
}
return Promise.reject(error);
}
);
Service Layer Pattern¶
Services encapsulate API calls:
// services/wordlists.ts
import { api } from './api';
import { Wordlist, WordlistUploadData } from '../types/wordlists';
export const wordlistService = {
getWordlists: async (): Promise<Wordlist[]> => {
const response = await api.get('/api/wordlists');
return response.data;
},
uploadWordlist: async (
formData: FormData,
onProgress?: (progress: number) => void
): Promise<Wordlist> => {
const response = await api.post('/api/wordlists/upload', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
onUploadProgress: (progressEvent) => {
if (progressEvent.total) {
const progress = Math.round((progressEvent.loaded * 100) / progressEvent.total);
onProgress?.(progress);
}
},
});
return response.data;
},
updateWordlist: async (id: string, data: Partial<Wordlist>): Promise<Wordlist> => {
const response = await api.put(`/api/wordlists/${id}`, data);
return response.data;
},
deleteWordlist: async (id: string): Promise<void> => {
await api.delete(`/api/wordlists/${id}`);
},
};
Authentication Service¶
Authentication is handled through a dedicated service:
// services/auth.ts
import { api } from './api';
import { LoginCredentials, AuthResponse } from '../types/auth';
export const authService = {
login: async (credentials: LoginCredentials): Promise<AuthResponse> => {
const response = await api.post('/api/login', credentials);
return response.data;
},
logout: async (): Promise<void> => {
await api.post('/api/logout');
},
isAuthenticated: async (): Promise<{ authenticated: boolean; role?: string }> => {
const response = await api.get('/api/check-auth');
return response.data;
},
refreshToken: async (): Promise<void> => {
await api.post('/api/refresh-token');
},
};
Material-UI Theming and Styling¶
Theme Configuration¶
The application uses a dark theme with custom overrides:
// styles/theme.ts
import { createTheme, Theme } from '@mui/material/styles';
const theme: Theme = createTheme({
palette: {
mode: 'dark',
primary: {
main: '#ff0000',
},
background: {
default: '#000000',
paper: '#121212',
},
text: {
primary: '#ffffff',
},
},
components: {
MuiCssBaseline: {
styleOverrides: {
body: {
backgroundColor: '#000000',
color: '#ffffff',
},
},
},
MuiDataGrid: {
styleOverrides: {
root: {
border: 'none',
backgroundColor: '#121212',
'& .MuiDataGrid-columnHeaders': {
backgroundColor: 'rgba(255, 255, 255, 0.08)',
},
'& .MuiDataGrid-row:hover': {
backgroundColor: 'rgba(255, 255, 255, 0.05)',
},
},
},
},
},
});
export default theme;
Component Styling¶
Use the sx
prop for component-specific styles:
<Box
sx={{
display: 'flex',
flexDirection: 'column',
gap: 2,
p: 3,
backgroundColor: 'background.paper',
borderRadius: 1,
'&:hover': {
backgroundColor: 'action.hover',
},
}}
>
{/* Content */}
</Box>
Responsive Design¶
Use Material-UI's responsive utilities:
<Grid container spacing={2}>
<Grid item xs={12} sm={6} md={4}>
{/* Content adapts to screen size */}
</Grid>
</Grid>
<Box
sx={{
width: { xs: '100%', sm: '60%', md: '40%' },
display: { xs: 'none', md: 'block' },
}}
>
{/* Responsive visibility and sizing */}
</Box>
TypeScript Conventions¶
Type Definitions¶
All types are defined in the types/
directory:
// types/wordlists.ts
export enum WordlistType {
GENERAL = 'general',
SPECIALIZED = 'specialized',
TARGETED = 'targeted',
CUSTOM = 'custom',
}
export interface Wordlist {
id: string;
name: string;
description: string;
wordlist_type: WordlistType;
file_size: number;
word_count: number;
verification_status: 'pending' | 'verified' | 'failed';
created_at: string;
updated_at: string;
}
export interface WordlistUploadData {
file: File;
name?: string;
description?: string;
wordlist_type: WordlistType;
}
Component Props¶
Always define interfaces for component props:
interface TableProps {
data: Wordlist[];
onEdit?: (wordlist: Wordlist) => void;
onDelete?: (id: string) => void;
loading?: boolean;
}
const WordlistTable: React.FC<TableProps> = ({
data,
onEdit,
onDelete,
loading = false
}) => {
// Component implementation
};
API Response Types¶
Define types for API responses:
// types/api.ts
export interface ApiResponse<T> {
data: T;
message?: string;
}
export interface PaginatedResponse<T> {
data: T[];
total: number;
page: number;
pageSize: number;
}
export interface ApiError {
error: string;
message: string;
statusCode: number;
}
Utility Types¶
Use TypeScript utility types effectively:
// Partial for update operations
type UpdateWordlistData = Partial<Omit<Wordlist, 'id' | 'created_at' | 'updated_at'>>;
// Pick for specific field selections
type WordlistSummary = Pick<Wordlist, 'id' | 'name' | 'word_count'>;
// Union types for status
type JobStatus = 'pending' | 'running' | 'completed' | 'failed';
Testing Approaches¶
Unit Testing with Jest and React Testing Library¶
Although the project doesn't have extensive tests yet, here's the recommended approach:
// WordlistTable.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import WordlistTable from './WordlistTable';
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
},
});
const wrapper = ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
);
describe('WordlistTable', () => {
const mockWordlists: Wordlist[] = [
{
id: '1',
name: 'Test Wordlist',
description: 'Test description',
wordlist_type: WordlistType.GENERAL,
file_size: 1024,
word_count: 100,
verification_status: 'verified',
created_at: '2024-01-01',
updated_at: '2024-01-01',
},
];
it('renders wordlist data correctly', () => {
render(
<WordlistTable data={mockWordlists} />,
{ wrapper }
);
expect(screen.getByText('Test Wordlist')).toBeInTheDocument();
expect(screen.getByText('100')).toBeInTheDocument();
});
it('calls onEdit when edit button is clicked', () => {
const handleEdit = jest.fn();
render(
<WordlistTable data={mockWordlists} onEdit={handleEdit} />,
{ wrapper }
);
fireEvent.click(screen.getByLabelText('Edit'));
expect(handleEdit).toHaveBeenCalledWith(mockWordlists[0]);
});
});
Integration Testing¶
Test components with API interactions:
// WordlistsManagement.integration.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import WordlistsManagement from './WordlistsManagement';
const server = setupServer(
rest.get('/api/wordlists', (req, res, ctx) => {
return res(
ctx.json({
data: [
{
id: '1',
name: 'Test Wordlist',
word_count: 100,
},
],
})
);
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
test('loads and displays wordlists', async () => {
render(<WordlistsManagement />);
await waitFor(() => {
expect(screen.getByText('Test Wordlist')).toBeInTheDocument();
});
});
Testing Custom Hooks¶
// useDebounce.test.ts
import { renderHook, act } from '@testing-library/react-hooks';
import useDebounce from './useDebounce';
describe('useDebounce', () => {
jest.useFakeTimers();
it('returns initial value immediately', () => {
const { result } = renderHook(() => useDebounce('test', 500));
expect(result.current).toBe('test');
});
it('debounces value changes', () => {
const { result, rerender } = renderHook(
({ value, delay }) => useDebounce(value, delay),
{ initialProps: { value: 'test', delay: 500 } }
);
rerender({ value: 'updated', delay: 500 });
expect(result.current).toBe('test');
act(() => {
jest.advanceTimersByTime(500);
});
expect(result.current).toBe('updated');
});
});
Running Tests¶
# Run all tests
npm test
# Run tests in watch mode
npm test -- --watch
# Run tests with coverage
npm test -- --coverage
# Run specific test file
npm test WordlistTable.test.tsx
Best Practices¶
Component Guidelines¶
- Keep components focused: Each component should have a single responsibility
- Use TypeScript strictly: Enable strict mode and avoid
any
types - Implement proper error handling: Use error boundaries and display user-friendly messages
- Optimize performance: Use React.memo, useCallback, and useMemo appropriately
- Follow accessibility standards: Use semantic HTML and ARIA attributes
Code Organization¶
- Consistent file naming: Use PascalCase for components, camelCase for utilities
- Co-locate related files: Keep tests, styles, and types near their components
- Extract reusable logic: Create custom hooks for shared functionality
- Document complex logic: Add JSDoc comments for non-trivial functions
Performance Optimization¶
- Lazy load routes: Use React.lazy for code splitting
- Optimize re-renders: Use React Query's staleTime and cacheTime
- Virtualize long lists: Use react-window for large datasets
- Debounce user input: Use the useDebounce hook for search fields
Security Considerations¶
- Sanitize user input: Validate and sanitize all user-provided data
- Use HTTPS: Ensure all API calls use secure connections
- Handle authentication properly: Store tokens securely and implement refresh logic
- Implement CSP: Configure Content Security Policy headers
Troubleshooting¶
Common Issues¶
- Certificate errors: The application includes a certificate check on startup
- CORS issues: Ensure the backend is configured to accept frontend origin
- Authentication loops: Check token refresh logic and API interceptors
- Build failures: Clear node_modules and package-lock.json, then reinstall
Debug Tips¶
- Enable React Developer Tools
- Use Redux DevTools for React Query debugging
- Check network tab for API call failures
- Review console for error messages