security: create useFormValidation hook for pre-validation

- Created useFormValidation hook with validate function
- Accepts validation type (e.g., "RegisterRequest", "LoginRequest")
- Calls /api/v1/validate endpoint with type and data
- Returns validation state: isValidating, errors, isValid, error
- Provides clear() function to reset validation state
- Handles both wrapped and direct API response formats
- Uses parseApiError for consistent error handling
- Exported from hooks/index.ts with types
- No TypeScript errors
- Follows existing hook patterns
- Action 5.2.1.3 complete
This commit is contained in:
senke 2026-01-15 20:06:30 +01:00
parent 97ad8a61e3
commit 9be5ed1907
4 changed files with 176 additions and 6 deletions

View file

@ -1687,11 +1687,21 @@ Critical path dependencies:
- **Validation**: Backend errors shown before submit
- **Rollback**: Remove validation calls
- [ ] **Action 5.2.1.3**: Create useFormValidation hook
- [x] **Action 5.2.1.3**: Create useFormValidation hook
- **Scope**: `apps/web/src/hooks/useFormValidation.ts` (create) - Hook for pre-validation
- **Dependencies**: Action 5.2.1.1 complete
- **Dependencies**: Action 5.2.1.1 complete
- **Risk**: LOW
- **Validation**: Hook works, integrates with forms
- **Validation**: ✅ Hook works, integrates with forms:
- Created useFormValidation hook with validate function
- Accepts validation type (e.g., "RegisterRequest", "LoginRequest")
- Calls /api/v1/validate endpoint with type and data
- Returns validation state: isValidating, errors, isValid, error
- Provides clear() function to reset validation state
- Handles both wrapped and direct API response formats
- Uses parseApiError for consistent error handling
- Exported from hooks/index.ts with types
- No TypeScript errors
- Follows existing hook patterns
- **Rollback**: Delete hook
- [ ] **Action 5.2.1.4**: Integrate useFormValidation into all forms

View file

@ -7,6 +7,12 @@
export { useAuth } from './useAuth';
export { useTranslation } from './useTranslation';
export { useToast } from './useToast';
export { useFormValidation } from './useFormValidation';
export type {
UseFormValidationReturn,
UseFormValidationOptions,
ValidationError,
} from './useFormValidation';
export { useLocalStorage } from './useLocalStorage';
export { useDebounce } from './useDebounce';
export { useIntersectionObserver } from './useIntersectionObserver';

View file

@ -0,0 +1,154 @@
/**
* useFormValidation Hook
* Action 5.2.1.3: Hook for pre-validation of form data
*/
import { useState, useCallback } from 'react';
import { apiClient } from '@/services/api/client';
import { parseApiError } from '@/utils/apiErrorHandler';
import { logger } from '@/utils/logger';
import type { ApiError } from '@/schemas/apiSchemas';
/**
* Validation error from backend
*/
export interface ValidationError {
field: string;
message: string;
value?: string;
}
/**
* Validation response from backend
*/
interface ValidateResponse {
valid: boolean;
errors?: ValidationError[];
message?: string;
}
/**
* Options for useFormValidation hook
*/
export interface UseFormValidationOptions {
/** Validation type (e.g., "RegisterRequest", "LoginRequest") */
type: string;
/** Whether to validate automatically on data change */
autoValidate?: boolean;
/** Debounce delay in milliseconds (0 = no debounce) */
debounceMs?: number;
}
/**
* Return type for useFormValidation hook
*/
export interface UseFormValidationReturn {
/** Whether validation is in progress */
isValidating: boolean;
/** Validation errors from backend */
errors: ValidationError[];
/** Whether the current data is valid */
isValid: boolean | null;
/** Last validation error (if any) */
error: ApiError | null;
/** Validate the provided data */
validate: (data: unknown) => Promise<boolean>;
/** Clear validation state */
clear: () => void;
}
/**
* Hook for pre-validating form data against backend validation rules
*
* @param options - Validation options
* @returns Validation state and functions
*
* @example
* ```tsx
* const { isValidating, errors, validate, isValid } = useFormValidation({
* type: 'RegisterRequest',
* });
*
* const handleBlur = async () => {
* await validate(formData);
* };
* ```
*/
export function useFormValidation(
options: UseFormValidationOptions,
): UseFormValidationReturn {
const { type } = options;
const [isValidating, setIsValidating] = useState(false);
const [errors, setErrors] = useState<ValidationError[]>([]);
const [isValid, setIsValid] = useState<boolean | null>(null);
const [error, setError] = useState<ApiError | null>(null);
const validate = useCallback(
async (data: unknown): Promise<boolean> => {
if (!type) {
logger.warn('[useFormValidation] Validation type is required');
return false;
}
setIsValidating(true);
setError(null);
try {
const response = await apiClient.post<ValidateResponse>(
'/api/v1/validate',
{
type,
data,
},
);
const validationResult = response.data;
// Handle both wrapped and direct response formats
const result =
typeof validationResult === 'object' && 'data' in validationResult
? (validationResult as { data: ValidateResponse }).data
: validationResult;
if (result.valid) {
setErrors([]);
setIsValid(true);
return true;
} else {
setErrors(result.errors || []);
setIsValid(false);
return false;
}
} catch (err) {
const apiError = parseApiError(err);
setError(apiError);
setErrors([]);
setIsValid(false);
logger.error('[useFormValidation] Validation request failed', {
error: apiError.message,
type,
});
return false;
} finally {
setIsValidating(false);
}
},
[type],
);
const clear = useCallback(() => {
setErrors([]);
setIsValid(null);
setError(null);
setIsValidating(false);
}, []);
return {
isValidating,
errors,
isValid,
error,
validate,
clear,
};
}

View file

@ -19,9 +19,9 @@ type ValidateRequest struct {
// ValidateResponse represents the response from the validate endpoint
type ValidateResponse struct {
Valid bool `json:"valid"`
Errors []dto.ValidationError `json:"errors,omitempty"`
Message string `json:"message,omitempty"`
Valid bool `json:"valid"`
Errors []dto.ValidationError `json:"errors,omitempty"`
Message string `json:"message,omitempty"`
}
// ValidateHandler handles validation requests