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:
parent
97ad8a61e3
commit
9be5ed1907
4 changed files with 176 additions and 6 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
154
apps/web/src/hooks/useFormValidation.ts
Normal file
154
apps/web/src/hooks/useFormValidation.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue