- Add real-time validation for WiFi credentials with password strength indicator - Add validateRealTime() for immediate UI feedback as user types - Add parseWiFiErrorResponse() for user-friendly error messages - Add prepareCredentialsForDevice() for pre-transmission validation - Block BLE protocol delimiter characters (| and ,) in credentials - Create WiFiPasswordInput component with strength bar and validation hints - Update BLEManager with improved error messages and pre-validation - Update MockBLEManager with simulation scenarios for testing - Add 62 comprehensive tests for all validation functions 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
413 lines
12 KiB
TypeScript
413 lines
12 KiB
TypeScript
/**
|
|
* WiFi Credentials Validation Utility
|
|
*
|
|
* Provides comprehensive validation for WiFi SSID and password
|
|
* following IEEE 802.11 standards and common router implementations.
|
|
*/
|
|
|
|
export interface WiFiValidationResult {
|
|
valid: boolean;
|
|
errors: string[];
|
|
warnings: string[];
|
|
}
|
|
|
|
export interface WiFiCredentials {
|
|
ssid: string;
|
|
password: string;
|
|
}
|
|
|
|
/**
|
|
* Validate WiFi SSID
|
|
*
|
|
* Rules:
|
|
* - Length: 1-32 characters (IEEE 802.11 standard)
|
|
* - Can contain: letters, numbers, spaces, and special characters
|
|
* - Cannot be empty
|
|
* - Should not contain control characters
|
|
*/
|
|
export function validateSSID(ssid: string): WiFiValidationResult {
|
|
const errors: string[] = [];
|
|
const warnings: string[] = [];
|
|
|
|
// Check for empty SSID
|
|
if (!ssid || ssid.trim().length === 0) {
|
|
errors.push('Network name (SSID) cannot be empty');
|
|
return { valid: false, errors, warnings };
|
|
}
|
|
|
|
// Check length (IEEE 802.11 standard: max 32 bytes)
|
|
// Note: UTF-8 characters can be multiple bytes
|
|
const byteLength = new Blob([ssid]).size;
|
|
if (byteLength > 32) {
|
|
errors.push('Network name is too long (max 32 bytes)');
|
|
}
|
|
|
|
// Check for control characters (ASCII 0-31)
|
|
if (/[\x00-\x1F]/.test(ssid)) {
|
|
errors.push('Network name contains invalid control characters');
|
|
}
|
|
|
|
// Warnings for potentially problematic SSIDs
|
|
if (ssid.startsWith(' ') || ssid.endsWith(' ')) {
|
|
warnings.push('Network name has leading or trailing spaces - this may cause connection issues');
|
|
}
|
|
|
|
if (/\s{2,}/.test(ssid)) {
|
|
warnings.push('Network name has multiple consecutive spaces');
|
|
}
|
|
|
|
return {
|
|
valid: errors.length === 0,
|
|
errors,
|
|
warnings,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Validate WiFi Password
|
|
*
|
|
* Rules based on WPA/WPA2/WPA3 standards:
|
|
* - WPA/WPA2-PSK: 8-63 characters (ASCII)
|
|
* - Open networks: no password required
|
|
* - WEP: 5 or 13 characters (ASCII) or 10/26 hex digits (deprecated)
|
|
*
|
|
* Additional checks:
|
|
* - Should not be empty for secured networks
|
|
* - Should not contain control characters
|
|
* - Common weak passwords warning
|
|
*/
|
|
export function validatePassword(
|
|
password: string,
|
|
authType?: string
|
|
): WiFiValidationResult {
|
|
const errors: string[] = [];
|
|
const warnings: string[] = [];
|
|
|
|
// Open networks don't need password
|
|
if (authType === 'Open') {
|
|
return { valid: true, errors, warnings };
|
|
}
|
|
|
|
// Check for empty password on secured networks
|
|
if (!password || password.length === 0) {
|
|
errors.push('Password is required for secured networks');
|
|
return { valid: false, errors, warnings };
|
|
}
|
|
|
|
// WPA/WPA2/WPA3 validation (most common)
|
|
if (!authType || authType.includes('WPA')) {
|
|
// Length check: 8-63 characters
|
|
if (password.length < 8) {
|
|
errors.push('Password must be at least 8 characters for WPA/WPA2/WPA3 networks');
|
|
}
|
|
|
|
if (password.length > 63) {
|
|
errors.push('Password cannot exceed 63 characters for WPA/WPA2/WPA3 networks');
|
|
}
|
|
|
|
// Check for ASCII only (non-ASCII can cause issues)
|
|
|
|
if (!/^[\x20-\x7E]*$/.test(password)) {
|
|
warnings.push('Password contains non-ASCII characters - this may cause issues on some devices');
|
|
}
|
|
}
|
|
|
|
// WEP validation (deprecated but still exists)
|
|
if (authType === 'WEP') {
|
|
const isValidWEP =
|
|
password.length === 5 || // 40-bit WEP
|
|
password.length === 13 || // 104-bit WEP
|
|
(password.length === 10 && /^[0-9A-Fa-f]+$/.test(password)) || // 40-bit hex
|
|
(password.length === 26 && /^[0-9A-Fa-f]+$/.test(password)); // 104-bit hex
|
|
|
|
if (!isValidWEP) {
|
|
errors.push('WEP password must be 5 or 13 ASCII characters, or 10/26 hexadecimal digits');
|
|
}
|
|
|
|
warnings.push('WEP is deprecated and insecure - consider upgrading your router to WPA2/WPA3');
|
|
}
|
|
|
|
// Check for control characters
|
|
if (/[\x00-\x1F]/.test(password)) {
|
|
errors.push('Password contains invalid control characters');
|
|
}
|
|
|
|
// Weak password warnings
|
|
if (password.length < 12 && !errors.length) {
|
|
warnings.push('Password is weak - consider using at least 12 characters');
|
|
}
|
|
|
|
// Common weak passwords
|
|
const weakPasswords = [
|
|
'password', '12345678', 'qwerty123', 'abc123456',
|
|
'password1', 'password123', '11111111', '00000000'
|
|
];
|
|
if (weakPasswords.includes(password.toLowerCase())) {
|
|
warnings.push('This is a commonly used weak password');
|
|
}
|
|
|
|
// Check for only numbers
|
|
if (/^\d+$/.test(password) && password.length === 8) {
|
|
warnings.push('Password contains only numbers - consider adding letters and symbols');
|
|
}
|
|
|
|
return {
|
|
valid: errors.length === 0,
|
|
errors,
|
|
warnings,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Validate complete WiFi credentials (SSID + password)
|
|
*/
|
|
export function validateWiFiCredentials(
|
|
ssid: string,
|
|
password: string,
|
|
authType?: string
|
|
): WiFiValidationResult {
|
|
const ssidResult = validateSSID(ssid);
|
|
const passwordResult = validatePassword(password, authType);
|
|
|
|
return {
|
|
valid: ssidResult.valid && passwordResult.valid,
|
|
errors: [...ssidResult.errors, ...passwordResult.errors],
|
|
warnings: [...ssidResult.warnings, ...passwordResult.warnings],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Check if password is strong enough for production use
|
|
* More strict than basic validation
|
|
*/
|
|
export function isStrongPassword(password: string): boolean {
|
|
if (password.length < 12) return false;
|
|
|
|
const hasUppercase = /[A-Z]/.test(password);
|
|
const hasLowercase = /[a-z]/.test(password);
|
|
const hasNumber = /\d/.test(password);
|
|
const hasSpecial = /[^A-Za-z0-9]/.test(password);
|
|
|
|
// Require at least 3 of 4 character types
|
|
const typesCount = [hasUppercase, hasLowercase, hasNumber, hasSpecial].filter(Boolean).length;
|
|
return typesCount >= 3;
|
|
}
|
|
|
|
/**
|
|
* Sanitize WiFi credentials before sending to device
|
|
* Trims whitespace and performs basic cleanup
|
|
*/
|
|
export function sanitizeWiFiCredentials(credentials: WiFiCredentials): WiFiCredentials {
|
|
return {
|
|
ssid: credentials.ssid.trim(),
|
|
password: credentials.password, // Don't trim password - spaces might be intentional
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get user-friendly error message for validation result
|
|
*/
|
|
export function getValidationErrorMessage(result: WiFiValidationResult): string {
|
|
if (result.valid) return '';
|
|
|
|
if (result.errors.length > 0) {
|
|
return result.errors[0]; // Return first error
|
|
}
|
|
|
|
return 'Invalid WiFi credentials';
|
|
}
|
|
|
|
/**
|
|
* Get combined error and warning message
|
|
*/
|
|
export function getValidationMessage(result: WiFiValidationResult): string {
|
|
const messages: string[] = [];
|
|
|
|
if (result.errors.length > 0) {
|
|
messages.push(...result.errors);
|
|
}
|
|
|
|
if (result.warnings.length > 0) {
|
|
messages.push(...result.warnings);
|
|
}
|
|
|
|
return messages.join('\n');
|
|
}
|
|
|
|
/**
|
|
* Real-time validation result for UI feedback
|
|
*/
|
|
export interface RealTimeValidationState {
|
|
ssidValid: boolean;
|
|
ssidError: string | null;
|
|
passwordValid: boolean;
|
|
passwordError: string | null;
|
|
passwordStrength: 'weak' | 'medium' | 'strong' | null;
|
|
canSubmit: boolean;
|
|
warnings: string[];
|
|
}
|
|
|
|
/**
|
|
* Perform real-time validation as user types
|
|
* Returns a simplified state for UI updates
|
|
*/
|
|
export function validateRealTime(
|
|
ssid: string,
|
|
password: string,
|
|
authType?: string
|
|
): RealTimeValidationState {
|
|
const result: RealTimeValidationState = {
|
|
ssidValid: true,
|
|
ssidError: null,
|
|
passwordValid: true,
|
|
passwordError: null,
|
|
passwordStrength: null,
|
|
canSubmit: false,
|
|
warnings: [],
|
|
};
|
|
|
|
// SSID validation (only show error if user has started typing)
|
|
if (ssid.length > 0) {
|
|
const ssidResult = validateSSID(ssid);
|
|
result.ssidValid = ssidResult.valid;
|
|
result.ssidError = ssidResult.errors.length > 0 ? ssidResult.errors[0] : null;
|
|
result.warnings.push(...ssidResult.warnings);
|
|
}
|
|
|
|
// Password validation
|
|
if (authType === 'Open') {
|
|
// Open networks don't need password
|
|
result.passwordValid = true;
|
|
result.passwordStrength = null;
|
|
} else if (password.length > 0) {
|
|
const passwordResult = validatePassword(password, authType);
|
|
result.passwordValid = passwordResult.valid;
|
|
result.passwordError = passwordResult.errors.length > 0 ? passwordResult.errors[0] : null;
|
|
result.warnings.push(...passwordResult.warnings);
|
|
|
|
// Calculate password strength for visual feedback
|
|
if (password.length >= 8) {
|
|
if (isStrongPassword(password)) {
|
|
result.passwordStrength = 'strong';
|
|
} else if (password.length >= 10) {
|
|
result.passwordStrength = 'medium';
|
|
} else {
|
|
result.passwordStrength = 'weak';
|
|
}
|
|
}
|
|
}
|
|
|
|
// Can submit if both are valid (or password not required for open networks)
|
|
const ssidOk = ssid.length > 0 && result.ssidValid;
|
|
const passwordOk = authType === 'Open' || (password.length >= 8 && result.passwordValid);
|
|
result.canSubmit = ssidOk && passwordOk;
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Parse BLE WiFi error response and return user-friendly message
|
|
*/
|
|
export function parseWiFiErrorResponse(errorMessage: string): {
|
|
type: 'password_wrong' | 'network_not_found' | 'timeout' | 'connection_failed' | 'unknown';
|
|
message: string;
|
|
suggestion: string;
|
|
} {
|
|
const lowerError = errorMessage.toLowerCase();
|
|
|
|
// Password rejection
|
|
if (
|
|
lowerError.includes('credentials rejected') ||
|
|
lowerError.includes('wrong password') ||
|
|
lowerError.includes('check password') ||
|
|
lowerError.includes('w|fail')
|
|
) {
|
|
return {
|
|
type: 'password_wrong',
|
|
message: 'WiFi password is incorrect',
|
|
suggestion: 'Please double-check the password and try again. Make sure caps lock is off.',
|
|
};
|
|
}
|
|
|
|
// Network not found
|
|
if (
|
|
lowerError.includes('not found') ||
|
|
lowerError.includes('no network') ||
|
|
lowerError.includes('ssid not available')
|
|
) {
|
|
return {
|
|
type: 'network_not_found',
|
|
message: 'WiFi network not found',
|
|
suggestion: 'Make sure the sensor is within range of your WiFi router.',
|
|
};
|
|
}
|
|
|
|
// Timeout
|
|
if (
|
|
lowerError.includes('timeout') ||
|
|
lowerError.includes('timed out') ||
|
|
lowerError.includes('did not respond')
|
|
) {
|
|
return {
|
|
type: 'timeout',
|
|
message: 'Sensor did not respond in time',
|
|
suggestion: 'Move closer to the sensor and try again. Make sure it is powered on.',
|
|
};
|
|
}
|
|
|
|
// Connection failed
|
|
if (
|
|
lowerError.includes('connect') ||
|
|
lowerError.includes('ble') ||
|
|
lowerError.includes('bluetooth')
|
|
) {
|
|
return {
|
|
type: 'connection_failed',
|
|
message: 'Lost connection to sensor',
|
|
suggestion: 'Please try again. If the problem persists, restart the sensor.',
|
|
};
|
|
}
|
|
|
|
// Unknown error
|
|
return {
|
|
type: 'unknown',
|
|
message: 'WiFi configuration failed',
|
|
suggestion: 'Please try again. If the problem persists, contact support.',
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Validate WiFi credentials and prepare for BLE transmission
|
|
* Returns sanitized credentials or throws with detailed error
|
|
*/
|
|
export function prepareCredentialsForDevice(
|
|
ssid: string,
|
|
password: string,
|
|
authType?: string
|
|
): { ssid: string; password: string } {
|
|
// Full validation
|
|
const validation = validateWiFiCredentials(ssid, password, authType);
|
|
|
|
if (!validation.valid) {
|
|
const errorMsg = getValidationErrorMessage(validation);
|
|
throw new Error(errorMsg);
|
|
}
|
|
|
|
// Sanitize
|
|
const sanitized = sanitizeWiFiCredentials({ ssid, password });
|
|
|
|
// Additional safety checks for BLE transmission
|
|
// Check for characters that might cause parsing issues in BLE protocol
|
|
// Protocol uses | and , as delimiters: W|SSID,PASSWORD
|
|
if (sanitized.ssid.includes('|') || sanitized.ssid.includes(',')) {
|
|
throw new Error('Network name contains invalid characters (| or ,)');
|
|
}
|
|
|
|
if (sanitized.password.includes('|')) {
|
|
// Password can contain comma but not pipe (delimiter)
|
|
throw new Error('Password contains invalid character (|)');
|
|
}
|
|
|
|
return sanitized;
|
|
}
|