fix issues
This commit is contained in:
274
apps/website/lib/utils/errorUtils.ts
Normal file
274
apps/website/lib/utils/errorUtils.ts
Normal file
@@ -0,0 +1,274 @@
|
||||
/**
|
||||
* Enhanced Error Utilities for GridPilot
|
||||
*
|
||||
* Provides comprehensive error handling, validation, and user-friendly error messages
|
||||
* for both end users and developers.
|
||||
*/
|
||||
|
||||
import { ApiError } from '@/lib/api/base/ApiError';
|
||||
|
||||
export interface ValidationError {
|
||||
field: string;
|
||||
message: string;
|
||||
value?: unknown;
|
||||
}
|
||||
|
||||
export interface EnhancedErrorContext {
|
||||
timestamp?: string;
|
||||
component?: string;
|
||||
action?: string;
|
||||
formData?: Record<string, unknown>;
|
||||
userId?: string;
|
||||
sessionId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse API error response to extract validation errors or user-friendly messages
|
||||
*/
|
||||
export function parseApiError(error: unknown): {
|
||||
userMessage: string;
|
||||
developerMessage: string;
|
||||
validationErrors: ValidationError[];
|
||||
isValidationError: boolean;
|
||||
} {
|
||||
const result = {
|
||||
userMessage: 'An unexpected error occurred',
|
||||
developerMessage: '',
|
||||
validationErrors: [] as ValidationError[],
|
||||
isValidationError: false,
|
||||
};
|
||||
|
||||
if (error instanceof ApiError) {
|
||||
result.developerMessage = error.getDeveloperMessage();
|
||||
|
||||
// Check if it's a validation error
|
||||
if (error.type === 'VALIDATION_ERROR') {
|
||||
result.isValidationError = true;
|
||||
result.userMessage = 'Please check your input and try again';
|
||||
|
||||
// Try to parse validation details from response
|
||||
try {
|
||||
if (error.context.responseText) {
|
||||
const parsed = JSON.parse(error.context.responseText);
|
||||
|
||||
// Handle NestJS validation error format
|
||||
if (parsed.message && Array.isArray(parsed.message)) {
|
||||
result.validationErrors = parsed.message.map((msg: any) => ({
|
||||
field: msg.property || msg.field || 'unknown',
|
||||
message: msg.constraints ? Object.values(msg.constraints).join(', ') : msg.message || 'Invalid value',
|
||||
value: msg.value,
|
||||
}));
|
||||
}
|
||||
// Handle custom error format
|
||||
else if (parsed.errors && Array.isArray(parsed.errors)) {
|
||||
result.validationErrors = parsed.errors.map((err: any) => ({
|
||||
field: err.field || err.property || 'unknown',
|
||||
message: err.message || 'Invalid value',
|
||||
value: err.value,
|
||||
}));
|
||||
}
|
||||
// Handle single message
|
||||
else if (parsed.message && typeof parsed.message === 'string') {
|
||||
result.userMessage = parsed.message;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// If parsing fails, use default messages
|
||||
}
|
||||
} else {
|
||||
result.userMessage = error.getUserMessage();
|
||||
}
|
||||
} else if (error instanceof Error) {
|
||||
result.userMessage = error.message;
|
||||
result.developerMessage = error.message;
|
||||
} else {
|
||||
result.userMessage = 'An unknown error occurred';
|
||||
result.developerMessage = String(error);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format validation errors for display in forms
|
||||
*/
|
||||
export function formatValidationErrorsForForm(
|
||||
validationErrors: ValidationError[]
|
||||
): Record<string, string> {
|
||||
const formErrors: Record<string, string> = {};
|
||||
|
||||
validationErrors.forEach((error) => {
|
||||
// Map API field names to form field names
|
||||
const fieldName = mapApiFieldToFormField(error.field);
|
||||
formErrors[fieldName] = error.message;
|
||||
});
|
||||
|
||||
return formErrors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map API field names to form field names
|
||||
*/
|
||||
function mapApiFieldToFormField(apiField: string): string {
|
||||
const fieldMap: Record<string, string> = {
|
||||
'rememberMe': 'rememberMe',
|
||||
'email': 'email',
|
||||
'password': 'password',
|
||||
'displayName': 'displayName',
|
||||
'firstName': 'firstName',
|
||||
'lastName': 'lastName',
|
||||
'confirmPassword': 'confirmPassword',
|
||||
'role': 'role',
|
||||
};
|
||||
|
||||
return fieldMap[apiField] || apiField;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create enhanced error context for debugging
|
||||
*/
|
||||
export function createErrorContext(
|
||||
error: unknown,
|
||||
context: EnhancedErrorContext
|
||||
): EnhancedErrorContext {
|
||||
return {
|
||||
timestamp: new Date().toISOString(),
|
||||
...context,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if error is retryable
|
||||
*/
|
||||
export function isRetryable(error: unknown): boolean {
|
||||
if (error instanceof ApiError) {
|
||||
return error.isRetryable();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if error is a network connectivity issue
|
||||
*/
|
||||
export function isConnectivityError(error: unknown): boolean {
|
||||
if (error instanceof ApiError) {
|
||||
return error.isConnectivityIssue();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get error severity for logging and display
|
||||
*/
|
||||
export function getErrorSeverity(error: unknown): 'error' | 'warning' | 'info' {
|
||||
if (error instanceof ApiError) {
|
||||
const severity = error.getSeverity();
|
||||
if (severity === 'error') return 'error';
|
||||
if (severity === 'warn') return 'warning';
|
||||
return 'info';
|
||||
}
|
||||
return 'error';
|
||||
}
|
||||
|
||||
/**
|
||||
* Create user-friendly error summary
|
||||
*/
|
||||
export function createUserErrorSummary(error: unknown): {
|
||||
title: string;
|
||||
description: string;
|
||||
action: string;
|
||||
} {
|
||||
const parsed = parseApiError(error);
|
||||
|
||||
if (parsed.isValidationError) {
|
||||
return {
|
||||
title: 'Invalid Input',
|
||||
description: parsed.userMessage,
|
||||
action: 'Please review your input and try again',
|
||||
};
|
||||
}
|
||||
|
||||
if (isConnectivityError(error)) {
|
||||
return {
|
||||
title: 'Connection Issue',
|
||||
description: 'Unable to connect to the server',
|
||||
action: 'Check your internet connection and try again',
|
||||
};
|
||||
}
|
||||
|
||||
if (isRetryable(error)) {
|
||||
return {
|
||||
title: 'Temporary Issue',
|
||||
description: parsed.userMessage,
|
||||
action: 'Please try again in a moment',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title: 'Error',
|
||||
description: parsed.userMessage,
|
||||
action: 'Please try again or contact support if the issue persists',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Log error with context (only in development)
|
||||
*/
|
||||
export function logErrorWithContext(
|
||||
error: unknown,
|
||||
context: EnhancedErrorContext
|
||||
): void {
|
||||
if (process.env.NODE_ENV !== 'development') return;
|
||||
|
||||
const parsed = parseApiError(error);
|
||||
const severity = getErrorSeverity(error);
|
||||
|
||||
console.group(`🚨 [${severity.toUpperCase()}] ${context.component || 'Unknown'} - ${context.action || 'Unknown action'}`);
|
||||
console.error('User Message:', parsed.userMessage);
|
||||
console.error('Developer Message:', parsed.developerMessage);
|
||||
if (parsed.validationErrors.length > 0) {
|
||||
console.error('Validation Errors:', parsed.validationErrors);
|
||||
}
|
||||
if (context.formData) {
|
||||
console.error('Form Data:', context.formData);
|
||||
}
|
||||
console.error('Context:', context);
|
||||
console.error('Original Error:', error);
|
||||
console.groupEnd();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delay execution for retry logic
|
||||
*/
|
||||
export async function delay(ms: number): Promise<void> {
|
||||
await new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retry operation with exponential backoff
|
||||
*/
|
||||
export async function retryWithBackoff<T>(
|
||||
operation: () => Promise<T>,
|
||||
maxRetries: number = 3,
|
||||
baseDelay: number = 1000
|
||||
): Promise<T> {
|
||||
let lastError: unknown;
|
||||
|
||||
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
return await operation();
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
|
||||
if (attempt === maxRetries || !isRetryable(error)) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Exponential backoff: 1s, 2s, 4s
|
||||
const delayMs = baseDelay * Math.pow(2, attempt);
|
||||
await delay(delayMs);
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError;
|
||||
}
|
||||
270
apps/website/lib/utils/validation.ts
Normal file
270
apps/website/lib/utils/validation.ts
Normal file
@@ -0,0 +1,270 @@
|
||||
/**
|
||||
* Form Validation Utilities for GridPilot
|
||||
*
|
||||
* Provides reusable validation functions and schemas for common form fields
|
||||
*/
|
||||
|
||||
export interface ValidationResult {
|
||||
isValid: boolean;
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
export interface ValidationRule<T> {
|
||||
validate: (value: T) => boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Email validation
|
||||
*/
|
||||
export const emailValidation = (email: string): ValidationResult => {
|
||||
const errors: string[] = [];
|
||||
|
||||
if (!email.trim()) {
|
||||
errors.push('Email is required');
|
||||
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
||||
errors.push('Invalid email format');
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: errors.length === 0,
|
||||
errors,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Password validation
|
||||
*/
|
||||
export const passwordValidation = (password: string): ValidationResult => {
|
||||
const errors: string[] = [];
|
||||
|
||||
if (!password) {
|
||||
errors.push('Password is required');
|
||||
} else {
|
||||
if (password.length < 8) {
|
||||
errors.push('Password must be at least 8 characters');
|
||||
}
|
||||
if (!/[a-z]/.test(password) || !/[A-Z]/.test(password)) {
|
||||
errors.push('Password must contain uppercase and lowercase letters');
|
||||
}
|
||||
if (!/\d/.test(password)) {
|
||||
errors.push('Password must contain at least one number');
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: errors.length === 0,
|
||||
errors,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Name validation (for display names, first names, last names)
|
||||
*/
|
||||
export const nameValidation = (name: string, field: string = 'Name'): ValidationResult => {
|
||||
const errors: string[] = [];
|
||||
const trimmed = name.trim();
|
||||
|
||||
if (!trimmed) {
|
||||
errors.push(`${field} is required`);
|
||||
} else if (trimmed.length < 2) {
|
||||
errors.push(`${field} must be at least 2 characters`);
|
||||
} else if (trimmed.length > 25) {
|
||||
errors.push(`${field} must be no more than 25 characters`);
|
||||
} else if (!/^[A-Za-z\-']+$/.test(trimmed)) {
|
||||
errors.push(`${field} can only contain letters, hyphens, and apostrophes`);
|
||||
} else if (/^(user|test|demo|guest|player)/i.test(trimmed)) {
|
||||
errors.push(`Please use your real ${field.toLowerCase()}, not a nickname`);
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: errors.length === 0,
|
||||
errors,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Confirm password validation
|
||||
*/
|
||||
export const confirmPasswordValidation = (
|
||||
password: string,
|
||||
confirmPassword: string
|
||||
): ValidationResult => {
|
||||
const errors: string[] = [];
|
||||
|
||||
if (!confirmPassword) {
|
||||
errors.push('Please confirm your password');
|
||||
} else if (password !== confirmPassword) {
|
||||
errors.push('Passwords do not match');
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: errors.length === 0,
|
||||
errors,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Login form validation
|
||||
*/
|
||||
export interface LoginFormValues {
|
||||
email: string;
|
||||
password: string;
|
||||
rememberMe?: boolean;
|
||||
}
|
||||
|
||||
export const validateLoginForm = (values: LoginFormValues): Record<string, string> => {
|
||||
const errors: Record<string, string> = {};
|
||||
|
||||
const emailResult = emailValidation(values.email);
|
||||
if (!emailResult.isValid) {
|
||||
errors.email = emailResult.errors[0];
|
||||
}
|
||||
|
||||
const passwordResult = passwordValidation(values.password);
|
||||
if (!passwordResult.isValid) {
|
||||
errors.password = passwordResult.errors[0];
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
/**
|
||||
* Signup form validation
|
||||
*/
|
||||
export interface SignupFormValues {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
password: string;
|
||||
confirmPassword: string;
|
||||
}
|
||||
|
||||
export const validateSignupForm = (values: SignupFormValues): Record<string, string> => {
|
||||
const errors: Record<string, string> = {};
|
||||
|
||||
const firstNameResult = nameValidation(values.firstName, 'First name');
|
||||
if (!firstNameResult.isValid) {
|
||||
errors.firstName = firstNameResult.errors[0];
|
||||
}
|
||||
|
||||
const lastNameResult = nameValidation(values.lastName, 'Last name');
|
||||
if (!lastNameResult.isValid) {
|
||||
errors.lastName = lastNameResult.errors[0];
|
||||
}
|
||||
|
||||
const emailResult = emailValidation(values.email);
|
||||
if (!emailResult.isValid) {
|
||||
errors.email = emailResult.errors[0];
|
||||
}
|
||||
|
||||
const passwordResult = passwordValidation(values.password);
|
||||
if (!passwordResult.isValid) {
|
||||
errors.password = passwordResult.errors[0];
|
||||
}
|
||||
|
||||
const confirmPasswordResult = confirmPasswordValidation(values.password, values.confirmPassword);
|
||||
if (!confirmPasswordResult.isValid) {
|
||||
errors.confirmPassword = confirmPasswordResult.errors[0];
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
/**
|
||||
* Password strength checker
|
||||
*/
|
||||
export interface PasswordStrength {
|
||||
score: number; // 0-5
|
||||
label: string;
|
||||
color: string;
|
||||
requirements: Array<{ met: boolean; label: string }>;
|
||||
}
|
||||
|
||||
export function checkPasswordStrength(password: string): PasswordStrength {
|
||||
let score = 0;
|
||||
const requirements = [
|
||||
{ met: password.length >= 8, label: 'At least 8 characters' },
|
||||
{ met: password.length >= 12, label: 'At least 12 characters' },
|
||||
{ met: /[a-z]/.test(password) && /[A-Z]/.test(password), label: 'Upper and lowercase letters' },
|
||||
{ met: /\d/.test(password), label: 'At least one number' },
|
||||
{ met: /[^a-zA-Z\d]/.test(password), label: 'At least one special character' },
|
||||
];
|
||||
|
||||
requirements.forEach(req => {
|
||||
if (req.met) score++;
|
||||
});
|
||||
|
||||
let label = 'Weak';
|
||||
let color = 'bg-red-500';
|
||||
|
||||
if (score <= 1) {
|
||||
label = 'Weak';
|
||||
color = 'bg-red-500';
|
||||
} else if (score <= 2) {
|
||||
label = 'Fair';
|
||||
color = 'bg-warning-amber';
|
||||
} else if (score <= 3) {
|
||||
label = 'Good';
|
||||
color = 'bg-primary-blue';
|
||||
} else {
|
||||
label = 'Strong';
|
||||
color = 'bg-performance-green';
|
||||
}
|
||||
|
||||
return {
|
||||
score,
|
||||
label,
|
||||
color,
|
||||
requirements,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Field validation helper for real-time validation
|
||||
*/
|
||||
export function createFieldValidator<T>(rules: Array<ValidationRule<T>>) {
|
||||
return (value: T): ValidationResult => {
|
||||
const errors: string[] = [];
|
||||
|
||||
for (const rule of rules) {
|
||||
if (!rule.validate(value)) {
|
||||
errors.push(rule.message);
|
||||
break; // Stop at first failure
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: errors.length === 0,
|
||||
errors,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Common validation rules
|
||||
*/
|
||||
export const requiredRule: ValidationRule<string> = {
|
||||
validate: (value) => value.trim().length > 0,
|
||||
message: 'This field is required',
|
||||
};
|
||||
|
||||
export const minLengthRule = (min: number): ValidationRule<string> => ({
|
||||
validate: (value) => value.length >= min,
|
||||
message: `Must be at least ${min} characters`,
|
||||
});
|
||||
|
||||
export const maxLengthRule = (max: number): ValidationRule<string> => ({
|
||||
validate: (value) => value.length <= max,
|
||||
message: `Must be no more than ${max} characters`,
|
||||
});
|
||||
|
||||
export const emailRule: ValidationRule<string> = {
|
||||
validate: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
|
||||
message: 'Invalid email format',
|
||||
};
|
||||
|
||||
export const noSpacesRule: ValidationRule<string> = {
|
||||
validate: (value) => !/\s/.test(value),
|
||||
message: 'No spaces allowed',
|
||||
};
|
||||
Reference in New Issue
Block a user